charlemagne
charlemagne

Reputation: 322

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?

Upvotes: 6

Views: 2643

Answers (3)

charlemagne
charlemagne

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

Simon Hawe
Simon Hawe

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

user2357112
user2357112

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

Related Questions