How can I type annotate a general nested TypedDict?

724 Views Asked by At

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?

1

There are 1 best solutions below

0
STerliakov On BEST ANSWER

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.