Search code examples
pythonmypy

Mypy: Using unions in mapping types does not work as expected


Consider the following code:

def foo(a: dict[str | tuple[str, str], str]) -> None:
    pass


def bar(b: dict[str, str]) -> None:
    foo(b)


def baz(b: dict[tuple[str, str], str]) -> None:
    foo(b)


foo({"foo": "bar"})
foo({("foo", "bar"): "bar"})

When checked with mypy in strict mode it produces the following errors:

file.py:6: error: Argument 1 to "foo" has incompatible type "Dict[str, str]"; expected "Dict[Union[str, Tuple[str, str]], str]"
file.py:9: error: Argument 1 to "foo" has incompatible type "Dict[Tuple[str, str], str]"; expected "Dict[Union[str, Tuple[str, str]], str]"

Which doesn't seem to make sense to me. The parameter is defined to accept a dict with either a string or a tuple as keys and strings as values. However, both variants are not accepted when explicitly annotated as such. They do however work when passing a dict like this directly to the function. It seems to me that mypy expects a dict that has to be able to have both options of the union as keys. I fail to understand why? If the constraints for the key are to be either a string or a tuple of to strings, passing either should be fine. Right? Am I missing something here?


Solution

  • A dict[str | tuple[str, str], str] isn't just a dict with either str or tuple[str, str] keys. It's a dict you can add more str or tuple[str, str] keys to.

    You can't add str keys to a dict[tuple[str, str], str], and you can't add tuple[str, str] keys to a dict[str, str], so those types aren't compatible.

    If you pass a literal dict directly to foo (or to bar or baz), that literal has no static type. mypy infers a type for the dict based on the context. Many different types may be inferred for a literal based on its context. When you pass b to foo inside bar or baz, b already has a static type, and that type is incompatible with foo's signature.