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?
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.