Reputation: 322
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?
Upvotes: 6
Views: 2643
Reputation: 322
So, I was finally able to figure out what the issue is. As I suspected after @user2357112 supports Monica's answer, Mapping
is in fact invariant on the key. There is no good reason for this other than it apparently being hard to implement because of how Mapping
itself is implemented.
Upvotes: 1
Reputation: 4539
What would work here is
def foo(a: dict[str, str] | dict[tuple[str, str], str]) -> None:
pass
Or you would have to help mypy and explicitly type annotate the dicts you are passing like
mydict : dict[str | tuple[str, str], str] = {"a" : "b"}
foo(mydict) # your foo as typed in your example
Upvotes: 1
Reputation: 281151
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.
Upvotes: 4