@@ -48,7 +48,7 @@ Another possible use case for this is a sound way to
4848 class Movie(TypedDict):
4949 name: str
5050 director: str
51-
51+
5252 class Book(TypedDict):
5353 name: str
5454 author: str
@@ -194,12 +194,12 @@ to the ``extra_items`` argument. For example::
194194
195195 class Movie(TypedDict, extra_items=bool):
196196 name: str
197-
197+
198198 a: Movie = {"name": "Blade Runner", "novel_adaptation": True} # OK
199199 b: Movie = {
200200 "name": "Blade Runner",
201201 "year": 1982, # Not OK. 'int' is not assignable to 'bool'
202- }
202+ }
203203
204204Here, ``extra_items=bool `` specifies that items other than ``'name' ``
205205have a value type of ``bool `` and are non-required.
@@ -214,12 +214,12 @@ the ``extra_items`` argument::
214214 def f(movie: Movie) -> None:
215215 reveal_type(movie["name"]) # Revealed type is 'str'
216216 reveal_type(movie["novel_adaptation"]) # Revealed type is 'bool'
217-
217+
218218``extra_items `` is inherited through subclassing::
219219
220220 class MovieBase(TypedDict, extra_items=int | None):
221221 name: str
222-
222+
223223 class Movie(MovieBase):
224224 year: int
225225
@@ -234,38 +234,46 @@ Here, ``'year'`` in ``a`` is an extra key defined on ``Movie`` whose value type
234234is ``int ``. ``'other_extra_key' `` in ``b `` is another extra key whose value type
235235must be assignable to the value of ``extra_items `` defined on ``MovieBase ``.
236236
237+ ``extra_items `` is also supported with the functional syntax::
238+
239+ Movie = TypedDict("Movie", {"name": str}, extra_items=int | None)
240+
237241The ``closed `` Class Parameter
238242------------------------------
239243
240- When ``closed=True `` is set, no extra items are allowed. This is a shorthand for
244+ When ``closed=True `` is set, no extra items are allowed. This is equivalent to
241245``extra_items=Never ``, because there can't be a value type that is assignable to
242- :class: `~typing.Never `.
246+ :class: `~typing.Never `. It is a runtime error to use the ``closed `` and
247+ ``extra_items `` parameters in the same TypedDict definition.
243248
244249Similar to ``total ``, only a literal ``True `` or ``False `` is supported as the
245- value of the ``closed `` argument; ``closed `` is ``False `` by default, which
246- preserves the previous TypedDict behavior.
250+ value of the ``closed `` argument. Type checkers should reject any non-literal value.
251+
252+ Passing ``closed=False `` explicitly requests the default TypedDict behavior,
253+ where arbitrary other keys may be present and subclasses may add arbitrary items.
254+ It is a type checker error to pass ``closed=False `` if a superclass has
255+ ``closed=True `` or sets ``extra_items ``.
247256
248- The value of ``closed `` is not inherited through subclassing, but the
249- implicitly set ``extra_items=Never `` is. It should be an error to use the
250- default ``closed=False `` when subclassing a closed TypedDict type::
257+ If ``closed `` is not provided, the behavior is inherited from the superclass.
258+ If the superclass is TypedDict itself or the superclass does not have ``closed=True ``
259+ or the ``extra_items `` parameter, the previous TypedDict behavior is preserved:
260+ arbitrary extra items are allowed. If the superclass has ``closed=True ``, the
261+ child class is also closed.
251262
252263 class BaseMovie(TypedDict, closed=True):
253264 name: str
254265
255- class MovieA(BaseMovie): # Not OK. An explicit ' closed=True' is required
266+ class MovieA(BaseMovie): # OK, still closed
256267 pass
257268
258- class MovieB(BaseMovie, closed=True): # OK
269+ class MovieB(BaseMovie, closed=True): # OK, but redundant
259270 pass
260271
261- Setting both ``closed `` and ``extra_items `` when defining a TypedDict type
262- should always be a runtime error::
263-
264- class Person(TypedDict, closed=False, extra_items=bool): # Not OK. 'closed' and 'extra_items' are incompatible
265- name: str
272+ class MovieC(BaseMovie, closed=False): # Type checker error
273+ pass
266274
267- As a consequence of ``closed=True `` being equivalent to ``extra_items=Never ``.
268- The same rules that apply to ``extra_items=Never `` should also apply to
275+ As a consequence of ``closed=True `` being equivalent to ``extra_items=Never ``,
276+ the same rules that apply to ``extra_items=Never `` also apply to
269277``closed=True ``. It is possible to use ``closed=True `` when subclassing if the
270278``extra_items `` argument is a read-only type::
271279
@@ -275,7 +283,7 @@ The same rules that apply to ``extra_items=Never`` should also apply to
275283 class MovieClosed(Movie, closed=True): # OK
276284 pass
277285
278- class MovieNever(Movie, extra_items=Never): # Not OK. 'closed=True' is preferred
286+ class MovieNever(Movie, extra_items=Never): # OK, but 'closed=True' is preferred
279287 pass
280288
281289This will be further discussed in
@@ -286,6 +294,10 @@ is assumed to allow non-required extra items of value type ``ReadOnly[object]``
286294during inheritance or assignability checks. This preserves the existing behavior
287295of TypedDict.
288296
297+ ``closed `` is also supported with the functional syntax::
298+
299+ Movie = TypedDict("Movie", {"name": str}, closed=True)
300+
289301Interaction with Totality
290302-------------------------
291303
@@ -315,7 +327,7 @@ function parameters still apply::
315327
316328 class Movie(TypedDict, extra_items=int):
317329 name: str
318-
330+
319331 def f(**kwargs: Unpack[Movie]) -> None: ...
320332
321333 # Should be equivalent to:
@@ -356,7 +368,7 @@ unless it is declared to be ``ReadOnly`` in the superclass::
356368
357369 class Parent(TypedDict, extra_items=int | None):
358370 pass
359-
371+
360372 class Child(Parent, extra_items=int): # Not OK. Like any other TypedDict item, extra_items's type cannot be changed
361373
362374Second, ``extra_items=T `` effectively defines the value type of any unnamed
@@ -378,20 +390,17 @@ added in a subclass, all of the following conditions should apply:
378390
379391 - The item's value type is :term: `typing:consistent ` with ``T ``
380392
381- - If ``extra_items `` is not overriden , the subclass inherits it as-is.
393+ - If ``extra_items `` is not overridden , the subclass inherits it as-is.
382394
383395For example::
384396
385397 class MovieBase(TypedDict, extra_items=int | None):
386398 name: str
387-
388- class AdaptedMovie(MovieBase): # Not OK. 'bool' is not assignable to 'int | None'
389- adapted_from_novel: bool
390-
391- class MovieRequiredYear(MovieBase): # Not OK. Required key 'year' is not known to 'Parent'
399+
400+ class MovieRequiredYear(MovieBase): # Not OK. Required key 'year' is not known to 'MovieBase'
392401 year: int | None
393402
394- class MovieNotRequiredYear(MovieBase): # Not OK. 'int | None' is not assignable to 'int'
403+ class MovieNotRequiredYear(MovieBase): # Not OK. 'int | None' is not consistent with 'int'
395404 year: NotRequired[int]
396405
397406 class MovieWithYear(MovieBase): # OK
@@ -478,7 +487,7 @@ checks::
478487 class MovieDetails(TypedDict, extra_items=int | None):
479488 name: str
480489 year: NotRequired[int]
481-
490+
482491 details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
483492 movie: Movie = details # Not OK. While 'int' is assignable to 'int | None',
484493 # 'int | None' is not assignable to 'int'
@@ -502,7 +511,7 @@ possible for an item to have a :term:`narrower <typing:narrow>` type than the
502511
503512 class Movie(TypedDict, extra_items=ReadOnly[str | int]):
504513 name: str
505-
514+
506515 class MovieDetails(TypedDict, extra_items=int):
507516 name: str
508517 year: NotRequired[int]
@@ -522,19 +531,19 @@ enforced::
522531
523532 class MovieExtraStr(TypedDict, extra_items=str):
524533 name: str
525-
534+
526535 extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
527536 extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
528537 extra_int = extra_str # Not OK. 'str' is not assignable to extra items type 'int'
529538 extra_str = extra_int # Not OK. 'int' is not assignable to extra items type 'str'
530-
539+
531540A non-closed TypedDict type implicitly allows non-required extra keys of value
532541type ``ReadOnly[object] ``. Applying the assignability rules between this type
533542and a closed TypedDict type is allowed::
534543
535544 class MovieNotClosed(TypedDict):
536545 name: str
537-
546+
538547 extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
539548 not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
540549 extra_int = not_closed # Not OK.
@@ -578,17 +587,13 @@ arguments of this type when constructed by calling the class object::
578587Interaction with Mapping[KT, VT]
579588--------------------------------
580589
581- A TypedDict type can be assignable to ``Mapping[KT, VT] `` types other than
582- ``Mapping[str, object] `` as long as all value types of the items on the
583- TypedDict type is :term: `typing:assignable ` to ``VT ``. This is an extension of this
590+ A TypedDict type is :term: `typing:assignable ` to a type of the form ``Mapping[str, VT] ``
591+ when all value types of the items in the TypedDict
592+ are assignable to ``VT ``. For the purpose of this rule, a
593+ TypedDict that does not have ``extra_items= `` or ``closed= `` set is considered
594+ to have an item with a value of type ``object ``. This extends the current
584595assignability rule from the `typing spec
585- <https://typing.readthedocs.io/en/latest/spec/typeddict.html#assignability> `__:
586-
587- * A TypedDict with all ``int `` values is not :term: `typing:assignable ` to
588- ``Mapping[str, int] ``, since there may be additional non-``int `` values
589- not visible through the type, due to :term: `typing:structural `
590- assignability. These can be accessed using the ``values() `` and
591- ``items() `` methods in ``Mapping ``,
596+ <https://typing.readthedocs.io/en/latest/spec/typeddict.html#assignability> `__.
592597
593598For example::
594599
@@ -598,6 +603,10 @@ For example::
598603 extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
599604 str_mapping: Mapping[str, str] = extra_str # OK
600605
606+ class MovieExtraInt(TypedDict, extra_items=int):
607+ name: str
608+
609+ extra_int: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
601610 int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
602611 int_str_mapping: Mapping[str, int | str] = extra_int # OK
603612
@@ -611,7 +620,7 @@ and ``items()`` on such TypedDict types::
611620Interaction with dict[KT, VT]
612621-----------------------------
613622
614- Note that because the presence of ``extra_items `` on a closed TypedDict type
623+ Because the presence of ``extra_items `` on a closed TypedDict type
615624prohibits additional required keys in its :term: `typing:structural `
616625:term: `typing:subtypes <subtype> `, we can determine if the TypedDict type and
617626its structural subtypes will ever have any required key during static analysis.
@@ -636,8 +645,8 @@ For example::
636645 def f(x: IntDict) -> None:
637646 v: dict[str, int] = x # OK
638647 v.clear() # OK
639-
640- not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
648+
649+ not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
641650 regular_dict: dict[str, int] = not_required_num_dict # OK
642651 f(not_required_num_dict) # OK
643652
@@ -652,10 +661,28 @@ because such dict can be a subtype of dict::
652661
653662 class CustomDict(dict[str, int]):
654663 pass
655-
664+
656665 not_a_regular_dict: CustomDict = {"num": 1}
657666 int_dict: IntDict = not_a_regular_dict # Not OK
658667
668+ Runtime behavior
669+ ----------------
670+
671+ At runtime, it is an error to pass both the ``closed `` and ``extra_items ``
672+ arguments in the same TypedDict definition, whether using the class syntax or
673+ the functional syntax. For simplicity, the runtime does not check other invalid
674+ combinations involving inheritance.
675+
676+ For introspection, the ``closed `` and ``extra_items `` arguments are mapped to
677+ two new attributes on the resulting TypedDict object: ``__closed__ `` and
678+ ``__extra_items__ ``. These attributes reflect exactly what was passed to the
679+ TypedDict constructor, without considering superclasses.
680+
681+ If ``closed `` is not passed, the value of ``__closed__ `` is None. If ``extra_items ``
682+ is not passed, the value of ``__extra_items__ `` is the new sentinel object
683+ ``typing.NoExtraItems ``. (It cannot be ``None ``, because ``extra_items=None `` is a
684+ valid definition that indicates all extra items must be ``None ``.)
685+
659686How to Teach This
660687=================
661688
@@ -673,7 +700,7 @@ Because ``extra_items`` is an opt-in feature, no existing codebase will break
673700due to this change.
674701
675702Note that ``closed `` and ``extra_items `` as keyword arguments do not collide
676- with othere keys when using something like
703+ with other keys when using something like
677704``TD = TypedDict("TD", foo=str, bar=int) ``, because this syntax has already
678705been removed in Python 3.13.
679706
0 commit comments