Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

False positive [arg-type] error when updating a dict with a TypedDict #9086

Closed
jamesbraza opened this issue Jul 1, 2020 · 3 comments
Closed

Comments

@jamesbraza
Copy link
Contributor

jamesbraza commented Jul 1, 2020

I have found that mypy raises an error when using update on a dict and passing in an instance of a TypedDict.

According to the mypy docs for TypedDict:

any TypedDict object is a subtype of (that is, compatible with) Mapping[str, object]

Thus, I think this is a false positive.


Repro

This code runs fine with Python 3.8.2 and mypy 0.782.

from typing import TypedDict

class Foo(TypedDict):
    baz: int

foo: Foo = {"baz": 9000}

# spam is a regular dict, yet mypy errors out when trying to add a TypedDict
# to it.  This doesn't make sense to me, when a regular dict should be like
# saying equal Dict[Any, Any]
spam = {"ham": {"eggs": 5}}
spam["ham"].update(foo)  # error: Argument 1 to "update" of "dict" has
# incompatible type "Foo"; expected "Mapping[str, int]"  [arg-type]

corge = {"plugh": 9001}
spam["ham"].update(corge)  # no error
@JukkaL
Copy link
Collaborator

JukkaL commented Jul 3, 2020

Since TypedDict objects use structural subtyping, there could be additional items not visible through the type Foo, with arbitrary value types. Thus Mapping[str, object] is correct (though it is unintuitive).

@JukkaL JukkaL closed this as completed Jul 3, 2020
@jamesbraza
Copy link
Contributor Author

jamesbraza commented Jul 8, 2020

I agree that Mapping[str, object] is correct. However, I think the issue I am raising might have been misunderstood.

In the above sample code, Foo is a subtype of TypeDict, which is a sub-type of dict as well. Why can't one add an instance ofFoo as the value for a dictionary via update? In my eyes, a plain ol' dict should not mind if calling .update and passing in a TypedDict.

Perhaps I am missing something, can you provide a more specific example of where updating a plain dict's value to be a TypedDict instance can go wrong?

@jamesbraza
Copy link
Contributor Author

I figured out my misunderstanding, and I now agree with Jukka that it's not a false positive.

TL;DR: one has to inform mypy using either a type comment or variable annotation that spam is a dict. Otherwise, it will infer the type of spam via a type variable substitution, and there will be a type mismatch.

I have explained my reasoning below:


Example 1 (original example, restated)

foo: Foo = {"baz": 9000}
reveal_type(foo)  # note: Revealed type is 'TypedDict('updating_with_typeddict.Foo', {'baz': builtins.int})'

spam = {"ham": {"eggs": 5}}
reveal_type(spam["ham"])  # note: Revealed type is 'builtins.dict*[builtins.str*, builtins.int*]'
spam["ham"].update(foo)  # error: Argument 1 to "update" of "dict" has incompatible type "Foo";
# expected "Mapping[str, int]"  [arg-type]

In my original post, I had assumed that the type of spam["ham"] was Dict[Any, Any]. However, mypy was actually doing a type variable substitution, to infer the type of spam["ham"] (as shown via the *).

This then gets to the Type Consistency section of PEP 589. One can read there to understand why mypy is raising an error.


Example 2 (with correct method)

Here is the other example from the original question. We can see corge has its type inferred, and again (for the same reasons as Example 1 above), mypy errors saying expected "Mapping[str, int]".

corge = {"plugh": 9001}
reveal_type(corge)  # note: Revealed type is 'builtins.dict[builtins.str*, builtins.int*]'
corge.update(foo)  # error: Argument 1 to "update" of "dict" has incompatible type "Foo";
# expected "Mapping[str, int]"  [arg-type]

spam["ham"].update(corge)  # no error, because the types match up

The correct method, in which mypy no longer raises an error, is to specify the desired type of corge using a type variable annotation. Now, mypy no longer infers the type of corge.

corge2: dict = {"plugh": 9001}
reveal_type(corge2)  # note: Revealed type is 'builtins.dict[Any, Any]'
corge2.update(foo)  # no error

Original Example Corrected

And to apply the same fix to the original example 1:

spam2: Dict[str, dict] = {"ham": {"eggs": 5}}
reveal_type(spam2["ham"])  # note: Revealed type is 'builtins.dict*[Any, Any]'
spam2["ham"].update(foo)  # no error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants