From reading through mypy
issues, it seems as if calling dict.update()
, and supplying a TypedDict
is not type safe. This does not make sense to me.
My question is (particularly from the 2nd issue, linked below):
TypedDict
is a dict
at runtime, why does mypy
complain about not being able to pass a TypedDict
into dict.update
, saying it expects a Mapping
?
dict
is like saying Dict[Any, Any]
, so why not add a TypedDict
as a dict value?dict.update(SomeTypedDict)
not type safe?There are two examples of this, found in mypy
issues:
This is a pretty subtle issue. The update call is arguably not type safe.
Since TypedDict objects use structural subtyping, there could be additional items not visible through the type
Foo
, with arbitrary value types. ThusMapping[str, object]
is correct (though it is unintuitive).
Sample Code from python/mypy #9086
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]
PEP 589 says that:
First, any
TypedDict
type is consistent withMapping[str, object]
. Second, aTypedDict
type A is consistent withTypedDict
B if A is structurally compatible with B.
A
TypedDict
with all int values is not consistent withMapping[str, int]
, since there may be additional non-int values not visible through the type, due to structural subtyping. These can be accessed using the values() and items() methods in Mapping, for example.
Example:
class A(TypedDict):
x: int
class B(TypedDict):
x: int
y: str
def sum_values(m: Mapping[str, int]) -> int:
n = 0
for v in m.values():
n += v # Runtime error
return n
def f(a: A) -> None:
sum_values(a) # Error: 'A' incompatible with Mapping[str, int]
b: B = {'x': 0, 'y': 'foo'}
f(b)
Update: Let us consider your sample
from typing import TypedDict
class Foo(TypedDict):
baz: int
foo: Foo = {"baz": 9000}
# spam here is not equal Dict[Any, Any]. mypy will infer type for it
# as Dict[str, Dict[str, int]]. Consequently, update() method for its
# item below will expect Mapping[str, int]
spam = {"ham": {"eggs": 5}}
# If you try to do empty dict as below which indeed has type Dict[Any, Any]
# spam = {} # mypy will complain on it "Need type annotation for 'spam'"
spam["ham"].update(foo) # error: Argument 1 to "update" of "dict" has
# incompatible type "Foo"; expected "Mapping[str, int]" [arg-type]