← Back to index

typeddicts_extra_items.py

True Positive
False Positive
False Negative
Optional (detected)
Warning or Info
TP: 8
FP: 21
FN: 25
Optional: 0 / 0
1# pyright: enableExperimentalFeatures=true
2from collections.abc import Mapping
3from typing_extensions import ReadOnly, TypedDict, Unpack
4from typing import assert_type, Required, NotRequired, Never
5
6
7# > For a TypedDict type that specifies
8# > ``extra_items``, during construction, the value type of each unknown item
9# is expected to be non-required and assignable to the ``extra_items`` argument.
11class Movie(TypedDict, extra_items=bool):
12 name: str
14a: Movie = {"name": "Blade Runner", "novel_adaptation": True} # OK
Unexpected error [invalid-key] Unknown key "novel_adaptation" for TypedDict `Movie`: Unknown key "novel_adaptation"
15b: Movie = {"name": "Blade Runner", "year": 1982} # E: 'int' is not assignable to 'bool'
[invalid-key] Unknown key "year" for TypedDict `Movie`: Unknown key "year"
17# > The alternative inline syntax is also supported::
19MovieFunctional = TypedDict("MovieFunctional", {"name": str}, extra_items=bool)
21c: MovieFunctional = {"name": "Blade Runner", "novel_adaptation": True} # OK
22d: MovieFunctional = {"name": "Blade Runner", "year": 1982} # E: 'int' is not assignable to 'bool'
Expected a ty diagnostic for this line
24# > Accessing extra items is allowed. Type checkers must infer their value type from
25# > the ``extra_items`` argument
27def movie_keys(movie: Movie) -> None:
28 assert_type(movie["name"], str)
29 assert_type(movie["novel_adaptation"], bool)
Unexpected error [type-assertion-failure] Type `bool` does not match asserted type `str` [invalid-key] Unknown key "novel_adaptation" for TypedDict `Movie`: Unknown key "novel_adaptation"
31# > ``extra_items`` is inherited through subclassing
33class MovieBase(TypedDict, extra_items=ReadOnly[int | None]):
34 name: str
36class InheritedMovie(MovieBase):
37 year: int
39e: InheritedMovie = {"name": "Blade Runner", "year": None} # E: 'None' is incompatible with 'int'
[invalid-argument-type] Invalid argument to key "year" with declared type `int` on TypedDict `InheritedMovie`: value of type `None`
40f: InheritedMovie = {
41 "name": "Blade Runner",
42 "year": 1982,
43 "other_extra_key": None,
Unexpected error [invalid-key] Unknown key "other_extra_key" for TypedDict `InheritedMovie`: Unknown key "other_extra_key"
44} # OK
46# > Similar to ``total``, only a literal ``True`` or ``False`` is supported as the
47# > value of the ``closed`` argument. Type checkers should reject any non-literal value.
49class IllegalTD(TypedDict, closed=42 == 42): # E: Argument to "closed" must be a literal True or False
Expected a ty diagnostic for this line
50 name: str
52# > Passing ``closed=False`` explicitly requests the default TypedDict behavior,
53# > where arbitrary other keys may be present and subclasses may add arbitrary items.
55class BaseTD(TypedDict, closed=False):
56 name: str
58class ChildTD(BaseTD): # OK
59 age: int
61# > It is a type checker error to pass ``closed=False`` if a superclass has
62# > ``closed=True`` or sets ``extra_items``.
64class ClosedBase(TypedDict, closed=True):
65 name: str
67class IllegalChild1(ClosedBase, closed=False): # E: Cannot set 'closed=False' when superclass is 'closed=True'
Expected a ty diagnostic for this line
68 pass
70class ExtraItemsBase(TypedDict, extra_items=int):
71 name: str
73class IllegalChild2(ExtraItemsBase, closed=False): # E: Cannot set 'closed=False' when superclass has 'extra_items'
Expected a ty diagnostic for this line
74 pass
76# > If ``closed`` is not provided, the behavior is inherited from the superclass.
77# > If the superclass is TypedDict itself or the superclass does not have ``closed=True``
78# > or the ``extra_items`` parameter, the previous TypedDict behavior is preserved:
79# > arbitrary extra items are allowed. If the superclass has ``closed=True``, the
80# > child class is also closed.
82class BaseMovie(TypedDict, closed=True):
83 name: str
85class MovieA(BaseMovie): # OK, still closed
86 pass
88class MovieB(BaseMovie, closed=True): # OK, but redundant
89 pass
91class MovieC(MovieA): # E[MovieC]
Expected a ty diagnostic for this line (tag 'MovieC')
92 age: int # E[MovieC]: "MovieC" is a closed TypedDict; extra key "age" not allowed
Expected a ty diagnostic for this line (tag 'MovieC')
94class MovieD(MovieB): # E[MovieD]
Expected a ty diagnostic for this line (tag 'MovieD')
95 age: int # E[MovieD]: "MovieD" is a closed TypedDict; extra key "age" not allowed
Expected a ty diagnostic for this line (tag 'MovieD')
97# > It is possible to use ``closed=True`` when subclassing if the ``extra_items``
98# > argument is a read-only type.
100class MovieES(TypedDict, extra_items=ReadOnly[str]):
101 pass
103class MovieClosed(MovieES, closed=True): # OK
104 pass
106class MovieNever(MovieES, extra_items=Never): # OK, but 'closed=True' is preferred
107 pass
109class IllegalCloseNonReadOnly(ExtraItemsBase, closed=True): # E: Cannot set 'closed=True' when superclass has non-read-only 'extra_items'
Expected a ty diagnostic for this line
110 pass
112# > It is an error to use ``Required[]`` or ``NotRequired[]`` with ``extra_items``.
114class IllegalExtraItemsTD(TypedDict, extra_items=Required[int]): # E: 'extra_items' value cannot be 'Required[...]'
Expected a ty diagnostic for this line
115 name: str
117class AnotherIllegalExtraItemsTD(TypedDict, extra_items=NotRequired[int]): # E: 'extra_items' value cannot be 'NotRequired[...]'
Expected a ty diagnostic for this line
118 name: str
120# > The extra items are non-required, regardless of the totality of the
121# > TypedDict. Operations that are available to ``NotRequired`` items should also be available to the
122# > extra items.
124class MovieEI(TypedDict, extra_items=int):
125 name: str
127def del_items(movie: MovieEI) -> None:
128 del movie["name"] # E: The value type of 'name' is 'Required[str]'
[invalid-argument-type] Cannot delete required key "name" from TypedDict `MovieEI`
129 del movie["year"] # OK: The value type of 'year' is 'NotRequired[int]'
Unexpected error [invalid-argument-type] Cannot delete unknown key "year" from TypedDict `MovieEI`
131# > For type checking purposes, ``Unpack[SomeTypedDict]`` with extra items should be
132# > treated as its equivalent in regular parameters.
134class MovieNoExtra(TypedDict):
135 name: str
137class MovieExtra(TypedDict, extra_items=int):
138 name: str
140def unpack_no_extra(**kwargs: Unpack[MovieNoExtra]) -> None: ...
141def unpack_extra(**kwargs: Unpack[MovieExtra]) -> None: ...
143unpack_no_extra(name="No Country for Old Men", year=2007) # E: Unrecognized item
Expected a ty diagnostic for this line
144unpack_extra(name="No Country for Old Men", year=2007) # OK
146# > Notably, if the TypedDict type specifies ``extra_items`` to be read-only,
147# > subclasses of the TypedDict type may redeclare ``extra_items``.
149class ReadOnlyBase(TypedDict, extra_items=ReadOnly[int]):
150 pass
152class ReadOnlyChild(ReadOnlyBase, extra_items=ReadOnly[bool]): # OK
153 pass
155class MutableChild(ReadOnlyBase, extra_items=int): # OK
156 pass
158# > Because a non-closed TypedDict type implicitly allows non-required extra items
159# > of value type ``ReadOnly[object]``, its subclass can override the
160# > ``extra_items`` argument with more specific types.
162class NonClosedBase(TypedDict):
163 name: str
165class SpecificExtraItems(NonClosedBase, extra_items=bytes): # OK
166 year: int
168# > First, it is not allowed to change the value of ``extra_items`` in a subclass
169# > unless it is declared to be ``ReadOnly`` in the superclass.
171class Parent(TypedDict, extra_items=int | None):
172 pass
174class Child(Parent, extra_items=int): # E: Cannot change 'extra_items' type unless it is 'ReadOnly' in the superclass
Expected a ty diagnostic for this line
175 pass
177# > Second, ``extra_items=T`` effectively defines the value type of any unnamed
178# > items accepted to the TypedDict and marks them as non-required. Thus, the above
179# > restriction applies to any additional items defined in a subclass.
181class MovieBase2(TypedDict, extra_items=int | None):
182 name: str
184class MovieRequiredYear(MovieBase2): # E[MovieRequiredYear]: Required key 'year' is not known to 'MovieBase'
Expected a ty diagnostic for this line (tag 'MovieRequiredYear')
185 year: int | None # E[MovieRequiredYear]
Expected a ty diagnostic for this line (tag 'MovieRequiredYear')
187class MovieNotRequiredYear(MovieBase2): # E[MovieNotRequiredYear]: 'int | None' is not consistent with 'int'
Expected a ty diagnostic for this line (tag 'MovieNotRequiredYear')
188 year: NotRequired[int] # E[MovieNotRequiredYear]
Expected a ty diagnostic for this line (tag 'MovieNotRequiredYear')
190class MovieWithYear(MovieBase2): # OK
191 year: NotRequired[int | None]
193class BookBase(TypedDict, extra_items=ReadOnly[int | None]):
194 name: str
196class BookWithPublisher(BookBase): # E[BookWithPublisher]: 'str' is not assignable to 'int | None'
Expected a ty diagnostic for this line (tag 'BookWithPublisher')
197 publisher: str # E[BookWithPublisher]
Expected a ty diagnostic for this line (tag 'BookWithPublisher')
199# > Let ``S`` be the set of keys of the explicitly defined items on a TypedDict
200# > type. If it specifies ``extra_items=T``, the TypedDict type is considered to
201# > have an infinite set of items that all satisfy the following conditions.
202# > - If ``extra_items`` is read-only:
203# > - The key's value type is :term:`assignable` to ``T``.
204# > - The key is not in ``S``.
205# > - If ``extra_items`` is not read-only:
206# > - The key is non-required.
207# > - The key's value type is :term:`consistent` with ``T``.
208# > - The key is not in ``S``.
210class MovieDetails(TypedDict, extra_items=int | None):
211 name: str
212 year: NotRequired[int]
214details2: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
215movie2: MovieBase2 = details2 # E: While 'int' is not consistent with 'int | None'
Expected a ty diagnostic for this line
217class MovieWithYear2(TypedDict, extra_items=int | None):
218 name: str
219 year: int | None
221details3: MovieWithYear2 = {"name": "Kill Bill Vol. 1", "year": 2003}
222movie3: MovieBase2 = details3 # E:'year' is not required in 'Movie', but it is required in 'MovieWithYear2'
Expected a ty diagnostic for this line
224# > When ``extra_items`` is specified to be read-only on a TypedDict type, it is
225# > possible for an item to have a :term:`narrower <narrow>` type than the
226# > ``extra_items`` argument.
228class MovieSI(TypedDict, extra_items=ReadOnly[str | int]):
229 name: str
231class MovieDetails4(TypedDict, extra_items=int):
232 name: str
233 year: NotRequired[int]
235class MovieDetails5(TypedDict, extra_items=int):
236 name: str
237 actors: list[str]
239details4: MovieDetails4 = {"name": "Kill Bill Vol. 2", "year": 2004}
240details5: MovieDetails5 = {"name": "Kill Bill Vol. 2", "actors": ["Uma Thurman"]}
241movie4: MovieSI = details4 # OK. 'int' is assignable to 'str | int'.
242movie5: MovieSI = details5 # E: 'list[str]' is not assignable to 'str | int'.
Expected a ty diagnostic for this line
244# > ``extra_items`` as a pseudo-item follows the same rules that other items have,
245# > so when both TypedDicts types specify ``extra_items``, this check is naturally
246# > enforced.
248class MovieExtraInt(TypedDict, extra_items=int):
249 name: str
251class MovieExtraStr(TypedDict, extra_items=str):
252 name: str
254extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
Unexpected error [invalid-key] Unknown key "year" for TypedDict `MovieExtraInt`: Unknown key "year"
255extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
Unexpected error [invalid-key] Unknown key "description" for TypedDict `MovieExtraStr`: Unknown key "description"
256extra_int = extra_str # E: 'str' is not assignable to extra items type 'int'
Expected a ty diagnostic for this line
257extra_str = extra_int # E: 'int' is not assignable to extra items type 'str'
Expected a ty diagnostic for this line
259# > A non-closed TypedDict type implicitly allows non-required extra keys of value
260# > type ``ReadOnly[object]``. Applying the assignability rules between this type
261# > and a closed TypedDict type is allowed.
263class MovieNotClosed(TypedDict):
264 name: str
266extra_int2: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
Unexpected error [invalid-key] Unknown key "year" for TypedDict `MovieExtraInt`: Unknown key "year"
267not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
268extra_int2 = not_closed # E: 'extra_items=ReadOnly[object]' implicitly on 'MovieNotClosed' is not assignable to with 'extra_items=int'
Expected a ty diagnostic for this line
269not_closed = extra_int2 # OK
271# > TypedDicts that allow extra items of type ``T`` also allow arbitrary keyword
272# > arguments of this type when constructed by calling the class object.
274class NonClosedMovie(TypedDict):
275 name: str
277NonClosedMovie(name="No Country for Old Men") # OK
278NonClosedMovie(name="No Country for Old Men", year=2007) # E: Unrecognized item
[invalid-key] Unknown key "year" for TypedDict `NonClosedMovie`: Unknown key "year"
280class ExtraMovie(TypedDict, extra_items=int):
281 name: str
283ExtraMovie(name="No Country for Old Men") # OK
284ExtraMovie(name="No Country for Old Men", year=2007) # OK
Unexpected error [invalid-key] Unknown key "year" for TypedDict `ExtraMovie`: Unknown key "year"
285ExtraMovie(name="No Country for Old Men", language="English") # E: Wrong type for extra item 'language'
[invalid-key] Unknown key "language" for TypedDict `ExtraMovie`: Unknown key "language"
287# This implies 'extra_items=Never',
288# so extra keyword arguments would produce an error
289class ClosedMovie(TypedDict, closed=True):
290 name: str
292ClosedMovie(name="No Country for Old Men") # OK
293ClosedMovie(name="No Country for Old Men", year=2007) # E: Extra items not allowed
[invalid-key] Unknown key "year" for TypedDict `ClosedMovie`: Unknown key "year"
295# > A TypedDict type is :term:`assignable` to a type of the form ``Mapping[str, VT]``
296# > when all value types of the items in the TypedDict
297# > are assignable to ``VT``.
299extra_str3: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
Unexpected error [invalid-key] Unknown key "summary" for TypedDict `MovieExtraStr`: Unknown key "summary"
300str_mapping: Mapping[str, str] = extra_str3 # OK
Unexpected error [invalid-assignment] Object of type `MovieExtraStr` is not assignable to `Mapping[str, str]`
302extra_int3: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
Unexpected error [invalid-key] Unknown key "year" for TypedDict `MovieExtraInt`: Unknown key "year"
303int_mapping: Mapping[str, int] = extra_int3 # E: 'int | str' is not assignable with 'int'
[invalid-assignment] Object of type `MovieExtraInt` is not assignable to `Mapping[str, int]`
304int_str_mapping: Mapping[str, int | str] = extra_int3 # OK
Unexpected error [invalid-assignment] Object of type `MovieExtraInt` is not assignable to `Mapping[str, int | str]`
306# > Type checkers should infer the precise signatures of ``values()`` and ``items()``
307# > on such TypedDict types.
309def foo(movie: MovieExtraInt) -> None:
310 assert_type(list(movie.items()), list[tuple[str, int | str]])
Unexpected error [type-assertion-failure] Type `list[tuple[str, int | str]]` does not match asserted type `list[tuple[str, object]]`
311 assert_type(list(movie.values()), list[int | str])
Unexpected error [type-assertion-failure] Type `list[int | str]` does not match asserted type `list[object]`
313# > The TypedDict type is :term:`assignable` to ``dict[str, VT]`` if all
314# > items on the TypedDict type satisfy the following conditions:
315# > - The value type of the item is :term:`consistent` with ``VT``.
316# > - The item is not read-only.
317# > - The item is not required.
319class IntDict(TypedDict, extra_items=int):
320 pass
322class IntDictWithNum(IntDict):
323 num: NotRequired[int]
325def clear_intdict(x: IntDict) -> None:
326 v: dict[str, int] = x # OK
Unexpected error [invalid-assignment] Object of type `IntDict` is not assignable to `dict[str, int]`
327 v.clear() # OK
329not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
Unexpected error [invalid-key] Unknown key "bar" for TypedDict `IntDictWithNum` - did you mean "num"?
330regular_dict: dict[str, int] = not_required_num_dict # OK
Unexpected error [invalid-assignment] Object of type `IntDictWithNum` is not assignable to `dict[str, int]`
331clear_intdict(not_required_num_dict) # OK
333# > In this case, methods that are previously unavailable on a TypedDict are allowed,
334# > with signatures matching ``dict[str, VT]``
335# > (e.g.: ``__setitem__(self, key: str, value: VT) -> None``).
337not_required_num_dict.clear() # OK
Unexpected error [unresolved-attribute] Object of type `IntDictWithNum` has no attribute `clear`
339assert_type(not_required_num_dict.popitem(), tuple[str, int])
Unexpected error [type-assertion-failure] Type `tuple[str, int]` does not match asserted type `Unknown` [unresolved-attribute] Object of type `IntDictWithNum` has no attribute `popitem`
341def nrnd(not_required_num_dict: IntDictWithNum, key: str):
342 not_required_num_dict[key] = 42 # OK
Unexpected error [invalid-key] TypedDict `IntDictWithNum` can only be subscripted with a string literal key, got key of type `str`.
343 del not_required_num_dict[key] # OK
Unexpected error [invalid-argument-type] Method `__delitem__` of type `(key: Literal["num"], /) -> None` cannot be called with key of type `str` on object of type `IntDictWithNum`
345# > ``dict[str, VT]`` is not assignable to a TypedDict type,
346# > because such dict can be a subtype of dict.
348class CustomDict(dict[str, int]):
349 pass
351def might_not_be(might_not_be_a_builtin_dict: dict[str, int]):
352 int_dict: IntDict = might_not_be_a_builtin_dict # E
[invalid-assignment] Object of type `dict[str, int]` is not assignable to `IntDict`
353 print(int_dict)
355not_a_builtin_dict = CustomDict({"num": 1})
356might_not_be(not_a_builtin_dict)