Hello. There is something in the assignment rules involving TypedDicts inheritance that I don’t understand, or that mypy gets wrong. Assume the following TypedDict declarations:
class A(typing.TypedDict, total=False):
a: int
class B(typing.TypedDict, total=False):
a: int
b: int
As expected, this is valid code:
a: A = {'a': 1}
b: B = {'a': 1}
However, unexpectedly for me, this is not:
a: A = {'a': 1}
b: B = a
At the same time:
b: B = {'a': 1, 'b': 2}
a: A = b
is considered valid, while
a: A = {'a': 1, 'b': 2}
is considered invalid, as it should.
I started looking into this because I wanted to define a TypedDict extending another with some not required fields but mypy does not like me to assign instances of the base dict to the extended one.
While B is a subtype of A, the converse is not true. That means B is assignable to A, but A is not assignable to B.
Keep in mind that TypedDicts are structural types. A value that conforms to A has a key named a, but it can also have any other keys with other names (including b), and the values for these keys can be any type. That means it’s not type safe to assign a value of type A to B because B indicates that the value associated with b must be an int.
For more details about TypeDict assignability rules, refer to the Python typing spec.
That code is considered invalid because of a special provision for dictionary literal expressions assigned to a TypedDict. Type checkers generate an error if a dictionary literal contains a key that is not found within the target TypedDict even though such a dictionary is technically type compatible with that TypedDict. This provision was introduced by mypy to help catch misspellings and typos, and all other major type checkers (including pyright, pytype and pyre) also implement this provision. See this mypy issue for historical context. [Note: I thought this behavior was dictated by the typing spec, but I can’t find it there. We should probably fix this omission.]
Here’s a modified code sample that demonstrates my point from above:
Thanks again, Eric. I found this behavior very confusing and contradicting the structural type principle. Mentioning it in the documentation would be a very good thing.
This leaves me wondering if what I would like to do is possible: is a way to type a dictionary B that should include only the keys found in a TypedDictA with the corresponding values having consistent types, plus a few more other optional keys, in a way that an instance of A can be assigned to a name annotated to have type B?