How can I type annotate a general nested TypedDict?
Question:
I’m trying to remove the Any
type hint from code similar to the following:
from typing import TypedDict, Any
class NestedDict(TypedDict):
foo: str
class EventDict(TypedDict):
nested: NestedDict
class BaseEventDict(TypedDict):
nested: Any # this should accept NestedDict but also other TypedDicts which may contain additional fields
test_dict: EventDict = {
"nested": {"foo": "abc"},
}
def print_dict(source_dict: BaseEventDict):
print(source_dict)
print_dict(test_dict)
Since the nested
field can contain either NestedDict
or other TypedDict
s with additional fields (for other EventDict
s), I’ve not been able to come up with a compatible TypedDict
(mypy
complains about extra keys). I thought Mapping[str, object]
might work in Any
‘s place, since [A]ny TypedDict type is consistent with Mapping[str, object]. However, mypy
complains with Argument 1 to "print_dict" has incompatible type "EventDict"; expected "BaseDict"
. Is there anything I can use instead of Any
, which essentially disables the check? Also, any insights into why Mapping[str, object]
is not a valid type here?
Answers:
Instead of using Any, you can define a new BaseDict type that accepts any TypedDict as long as its nested field is of type NestedDict:
from typing import Mapping, Type, Union, TypedDict
class NestedDict(TypedDict):
foo: str
class EventDict(TypedDict):
nested: NestedDict
class BaseDict(TypedDict):
nested: Union[NestedDict, Type[TypedDict[object, object]]]
test_dict: EventDict = {
"nested": {"foo": "abc"},
}
def print_dict(source_dict: BaseDict):
print(source_dict)
print_dict(test_dict)
The BaseDict type uses a Union to accept either a NestedDict or any TypedDict where the nested field is of type NestedDict. To achieve this, the Type function is used to refer to the TypedDict class without instantiating it.
Regarding your question about Mapping[str, object], it is not a valid type in this case because it is too generic. It would allow any key-value pair in the dictionary, not just the nested field. This would not be compatible with the EventDict type that has a specific structure.
TypedDict
fields are invariant, because TypedDict
is a mutable structure. The reasoning behind that is explained in PEP589 in detail. So, to accept a TypedDict
with a field of type "some TypedDict
or anything compatible with it" you can use a generic solution:
from __future__ import annotations
from typing import TypedDict, Generic, TypeVar
class NestedDict(TypedDict):
foo: str
_T = TypeVar('_T', bound=NestedDict)
class BaseEventDict(Generic[_T], TypedDict):
nested: _T # this should accept NestedDict but also other TypedDicts which may contain additional fields
BaseEventDict
is parametrized with a type of its field, which is bound to NestedDict
– this way T
can be substituted only with something compatible with NestedDict
. Let’s check:
class GoodNestedDict(TypedDict):
foo: str
bar: str
class BadNestedDict(TypedDict):
foo: int
class EventDict(TypedDict):
nested: NestedDict
class GoodEventDict(TypedDict):
nested: GoodNestedDict
class BadEventDict(TypedDict):
nested: BadNestedDict
# Funny case: lone TypeVar makes sense here
def print_dict(source_dict: BaseEventDict[_T]) -> None:
print(source_dict)
test_dict: EventDict = {
"nested": {"foo": "abc"},
}
good_test_dict: GoodEventDict = {
"nested": {"foo": "abc", "bar": "bar"},
}
bad_test_dict: BadEventDict = {
"nested": {"foo": 1},
}
print_dict(test_dict)
print_dict(good_test_dict)
print_dict(bad_test_dict) # E: Value of type variable "_T" of "print_dict" cannot be "BadNestedDict" [type-var]
In this setup print_dict
is also interesting: you cannot use an upper bound, because the field type is invariant, so a single TypeVar
with a bound (same as before) comes to rescue. Anything compatible with NestedDict
is accepted as _T
resolver, and everything incompatible is rejected.
Here’s a playground with this implementation.
I’m trying to remove the Any
type hint from code similar to the following:
from typing import TypedDict, Any
class NestedDict(TypedDict):
foo: str
class EventDict(TypedDict):
nested: NestedDict
class BaseEventDict(TypedDict):
nested: Any # this should accept NestedDict but also other TypedDicts which may contain additional fields
test_dict: EventDict = {
"nested": {"foo": "abc"},
}
def print_dict(source_dict: BaseEventDict):
print(source_dict)
print_dict(test_dict)
Since the nested
field can contain either NestedDict
or other TypedDict
s with additional fields (for other EventDict
s), I’ve not been able to come up with a compatible TypedDict
(mypy
complains about extra keys). I thought Mapping[str, object]
might work in Any
‘s place, since [A]ny TypedDict type is consistent with Mapping[str, object]. However, mypy
complains with Argument 1 to "print_dict" has incompatible type "EventDict"; expected "BaseDict"
. Is there anything I can use instead of Any
, which essentially disables the check? Also, any insights into why Mapping[str, object]
is not a valid type here?
Instead of using Any, you can define a new BaseDict type that accepts any TypedDict as long as its nested field is of type NestedDict:
from typing import Mapping, Type, Union, TypedDict
class NestedDict(TypedDict):
foo: str
class EventDict(TypedDict):
nested: NestedDict
class BaseDict(TypedDict):
nested: Union[NestedDict, Type[TypedDict[object, object]]]
test_dict: EventDict = {
"nested": {"foo": "abc"},
}
def print_dict(source_dict: BaseDict):
print(source_dict)
print_dict(test_dict)
The BaseDict type uses a Union to accept either a NestedDict or any TypedDict where the nested field is of type NestedDict. To achieve this, the Type function is used to refer to the TypedDict class without instantiating it.
Regarding your question about Mapping[str, object], it is not a valid type in this case because it is too generic. It would allow any key-value pair in the dictionary, not just the nested field. This would not be compatible with the EventDict type that has a specific structure.
TypedDict
fields are invariant, because TypedDict
is a mutable structure. The reasoning behind that is explained in PEP589 in detail. So, to accept a TypedDict
with a field of type "some TypedDict
or anything compatible with it" you can use a generic solution:
from __future__ import annotations
from typing import TypedDict, Generic, TypeVar
class NestedDict(TypedDict):
foo: str
_T = TypeVar('_T', bound=NestedDict)
class BaseEventDict(Generic[_T], TypedDict):
nested: _T # this should accept NestedDict but also other TypedDicts which may contain additional fields
BaseEventDict
is parametrized with a type of its field, which is bound to NestedDict
– this way T
can be substituted only with something compatible with NestedDict
. Let’s check:
class GoodNestedDict(TypedDict):
foo: str
bar: str
class BadNestedDict(TypedDict):
foo: int
class EventDict(TypedDict):
nested: NestedDict
class GoodEventDict(TypedDict):
nested: GoodNestedDict
class BadEventDict(TypedDict):
nested: BadNestedDict
# Funny case: lone TypeVar makes sense here
def print_dict(source_dict: BaseEventDict[_T]) -> None:
print(source_dict)
test_dict: EventDict = {
"nested": {"foo": "abc"},
}
good_test_dict: GoodEventDict = {
"nested": {"foo": "abc", "bar": "bar"},
}
bad_test_dict: BadEventDict = {
"nested": {"foo": 1},
}
print_dict(test_dict)
print_dict(good_test_dict)
print_dict(bad_test_dict) # E: Value of type variable "_T" of "print_dict" cannot be "BadNestedDict" [type-var]
In this setup print_dict
is also interesting: you cannot use an upper bound, because the field type is invariant, so a single TypeVar
with a bound (same as before) comes to rescue. Anything compatible with NestedDict
is accepted as _T
resolver, and everything incompatible is rejected.
Here’s a playground with this implementation.