Search code examples
pythonpython-3.xmypytypeddict

How can I type annotate a general nested TypedDict?


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?


Solution

  • 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.