1

I'm trying to add type hints in a dict type which has multiple fields that bind functions to them. e.g.

from typing import Dict, Callable, Any, Union

def fn():
  print("Hello World")

def fn2(name):
   print("goodbye world", name)

d = {
  "hello" : {
    "world": fn
  },
  "goodbye": {
    "world": fn2
  }
} # type: Dict[str, Dict[str, Union[Callable[[], None], Callable[[str], None]]]]

d["hello"]["world"]()
d["goodbye"]["world"]("john")

The problem I'm running to is whenever I try run mypy (v0.782) it throws error:

test2.py:17: error: Too few arguments
test2.py:18: error: Too many arguments

Clearly, I've passed the right argument as can be seen from the function definition and type hints. I'm clearly missing something for it to be throwing error.

However, the following works, so I suspect it has something to do with Union type in type hints.

from typing import Dict, Callable, Any, Union


def fn():
    print("Hello World")


d = {"hello": {"world": fn}}  # type: Dict[str, Dict[str, Callable[[], None]]]

d["hello"]["world"]()
shriek
  • 5,605
  • 8
  • 46
  • 75

1 Answers1

2

Let me remind you that problems like the one described in the question arise when the following constraints are not taken into account when working with Union:

Operations are valid for union types only if they are valid for every union item. This is why it’s often necessary to use an isinstance() check to first narrow down a union type to a non-union type. This also means that it’s recommended to avoid union types as function return types, since the caller may have to use isinstance() before doing anything interesting with the value.[1]

As a workaround, I could suggest using a single function with a optional argument. I use Protocol to define callback type with optional argument that impossible to express using the Callable[...]

from typing import Protocol, Optional, Dict


class Fn(Protocol):
    def __call__(self, name: Optional[str] = None) -> None:
        ...


def fn(name: Optional[str] = None) -> None:
    if name is None:
        print("Hello World")
    else:
        print("goodbye world", name)


d: Dict[str, Dict[str, Fn]] = {
    "hello": {
        "world": fn
    },
    "goodbye": {
        "world": fn
    }
}

d["hello"]["world"]()
d["goodbye"]["world"]("john")

[1] https://mypy.readthedocs.io/en/stable/kinds_of_types.html#union-types

shriek
  • 5,605
  • 8
  • 46
  • 75
alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • I like this however I can't help but feel it's a little restrictive. I wonder how it would look if I had not two but five functions with different size of parameters and return types? – shriek Sep 05 '20 at 05:18
  • Is there a limited set of keys for one function and also for others? – alex_noname Sep 08 '20 at 10:01
  • I can live with function being non-variadic. I'm just trying to wrap my head around on how the type would look if there were more than one property below `world` e.g. `{ "world": fn, "walk": fn2, "jump": fn3 }` in both `hello` and `goodbye` properties. – shriek Sep 10 '20 at 02:14
  • 1
    In such case you can use [TypedDict](https://www.python.org/dev/peps/pep-0589/) with a fixed set of keys – alex_noname Sep 13 '20 at 14:47