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 TypedDicts with additional fields (for other EventDicts), 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?

Asked By: Ewa Lipinska

||

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.

Answered By: Aayush Kataria

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.

Answered By: SUTerliakov
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.