@@ -48,7 +48,7 @@ Another possible use case for this is a sound way to
48
48
class Movie(TypedDict):
49
49
name: str
50
50
director: str
51
-
51
+
52
52
class Book(TypedDict):
53
53
name: str
54
54
author: str
@@ -194,12 +194,12 @@ to the ``extra_items`` argument. For example::
194
194
195
195
class Movie(TypedDict, extra_items=bool):
196
196
name: str
197
-
197
+
198
198
a: Movie = {"name": "Blade Runner", "novel_adaptation": True} # OK
199
199
b: Movie = {
200
200
"name": "Blade Runner",
201
201
"year": 1982, # Not OK. 'int' is not assignable to 'bool'
202
- }
202
+ }
203
203
204
204
Here, ``extra_items=bool `` specifies that items other than ``'name' ``
205
205
have a value type of ``bool `` and are non-required.
@@ -214,12 +214,12 @@ the ``extra_items`` argument::
214
214
def f(movie: Movie) -> None:
215
215
reveal_type(movie["name"]) # Revealed type is 'str'
216
216
reveal_type(movie["novel_adaptation"]) # Revealed type is 'bool'
217
-
217
+
218
218
``extra_items `` is inherited through subclassing::
219
219
220
220
class MovieBase(TypedDict, extra_items=int | None):
221
221
name: str
222
-
222
+
223
223
class Movie(MovieBase):
224
224
year: int
225
225
@@ -234,38 +234,46 @@ Here, ``'year'`` in ``a`` is an extra key defined on ``Movie`` whose value type
234
234
is ``int ``. ``'other_extra_key' `` in ``b `` is another extra key whose value type
235
235
must be assignable to the value of ``extra_items `` defined on ``MovieBase ``.
236
236
237
+ ``extra_items `` is also supported with the functional syntax::
238
+
239
+ Movie = TypedDict("Movie", {"name": str}, extra_items=int | None)
240
+
237
241
The ``closed `` Class Parameter
238
242
------------------------------
239
243
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
241
245
``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.
243
248
244
249
Similar 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 ``.
247
256
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.
251
262
252
263
class BaseMovie(TypedDict, closed=True):
253
264
name: str
254
265
255
- class MovieA(BaseMovie): # Not OK. An explicit ' closed=True' is required
266
+ class MovieA(BaseMovie): # OK, still closed
256
267
pass
257
268
258
- class MovieB(BaseMovie, closed=True): # OK
269
+ class MovieB(BaseMovie, closed=True): # OK, but redundant
259
270
pass
260
271
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
266
274
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
269
277
``closed=True ``. It is possible to use ``closed=True `` when subclassing if the
270
278
``extra_items `` argument is a read-only type::
271
279
@@ -275,7 +283,7 @@ The same rules that apply to ``extra_items=Never`` should also apply to
275
283
class MovieClosed(Movie, closed=True): # OK
276
284
pass
277
285
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
279
287
pass
280
288
281
289
This will be further discussed in
@@ -286,6 +294,10 @@ is assumed to allow non-required extra items of value type ``ReadOnly[object]``
286
294
during inheritance or assignability checks. This preserves the existing behavior
287
295
of TypedDict.
288
296
297
+ ``closed `` is also supported with the functional syntax::
298
+
299
+ Movie = TypedDict("Movie", {"name": str}, closed=True)
300
+
289
301
Interaction with Totality
290
302
-------------------------
291
303
@@ -315,7 +327,7 @@ function parameters still apply::
315
327
316
328
class Movie(TypedDict, extra_items=int):
317
329
name: str
318
-
330
+
319
331
def f(**kwargs: Unpack[Movie]) -> None: ...
320
332
321
333
# Should be equivalent to:
@@ -356,7 +368,7 @@ unless it is declared to be ``ReadOnly`` in the superclass::
356
368
357
369
class Parent(TypedDict, extra_items=int | None):
358
370
pass
359
-
371
+
360
372
class Child(Parent, extra_items=int): # Not OK. Like any other TypedDict item, extra_items's type cannot be changed
361
373
362
374
Second, ``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:
378
390
379
391
- The item's value type is :term: `typing:consistent ` with ``T ``
380
392
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.
382
394
383
395
For example::
384
396
385
397
class MovieBase(TypedDict, extra_items=int | None):
386
398
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'
392
401
year: int | None
393
402
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'
395
404
year: NotRequired[int]
396
405
397
406
class MovieWithYear(MovieBase): # OK
@@ -478,7 +487,7 @@ checks::
478
487
class MovieDetails(TypedDict, extra_items=int | None):
479
488
name: str
480
489
year: NotRequired[int]
481
-
490
+
482
491
details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
483
492
movie: Movie = details # Not OK. While 'int' is assignable to 'int | None',
484
493
# '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
502
511
503
512
class Movie(TypedDict, extra_items=ReadOnly[str | int]):
504
513
name: str
505
-
514
+
506
515
class MovieDetails(TypedDict, extra_items=int):
507
516
name: str
508
517
year: NotRequired[int]
@@ -522,19 +531,19 @@ enforced::
522
531
523
532
class MovieExtraStr(TypedDict, extra_items=str):
524
533
name: str
525
-
534
+
526
535
extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
527
536
extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
528
537
extra_int = extra_str # Not OK. 'str' is not assignable to extra items type 'int'
529
538
extra_str = extra_int # Not OK. 'int' is not assignable to extra items type 'str'
530
-
539
+
531
540
A non-closed TypedDict type implicitly allows non-required extra keys of value
532
541
type ``ReadOnly[object] ``. Applying the assignability rules between this type
533
542
and a closed TypedDict type is allowed::
534
543
535
544
class MovieNotClosed(TypedDict):
536
545
name: str
537
-
546
+
538
547
extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
539
548
not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
540
549
extra_int = not_closed # Not OK.
@@ -578,17 +587,13 @@ arguments of this type when constructed by calling the class object::
578
587
Interaction with Mapping[KT, VT]
579
588
--------------------------------
580
589
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
584
595
assignability 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> `__.
592
597
593
598
For example::
594
599
@@ -598,6 +603,10 @@ For example::
598
603
extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
599
604
str_mapping: Mapping[str, str] = extra_str # OK
600
605
606
+ class MovieExtraInt(TypedDict, extra_items=int):
607
+ name: str
608
+
609
+ extra_int: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
601
610
int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
602
611
int_str_mapping: Mapping[str, int | str] = extra_int # OK
603
612
@@ -611,7 +620,7 @@ and ``items()`` on such TypedDict types::
611
620
Interaction with dict[KT, VT]
612
621
-----------------------------
613
622
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
615
624
prohibits additional required keys in its :term: `typing:structural `
616
625
:term: `typing:subtypes <subtype> `, we can determine if the TypedDict type and
617
626
its structural subtypes will ever have any required key during static analysis.
@@ -636,8 +645,8 @@ For example::
636
645
def f(x: IntDict) -> None:
637
646
v: dict[str, int] = x # OK
638
647
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}
641
650
regular_dict: dict[str, int] = not_required_num_dict # OK
642
651
f(not_required_num_dict) # OK
643
652
@@ -652,10 +661,28 @@ because such dict can be a subtype of dict::
652
661
653
662
class CustomDict(dict[str, int]):
654
663
pass
655
-
664
+
656
665
not_a_regular_dict: CustomDict = {"num": 1}
657
666
int_dict: IntDict = not_a_regular_dict # Not OK
658
667
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
+
659
686
How to Teach This
660
687
=================
661
688
@@ -673,7 +700,7 @@ Because ``extra_items`` is an opt-in feature, no existing codebase will break
673
700
due to this change.
674
701
675
702
Note 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
677
704
``TD = TypedDict("TD", foo=str, bar=int) ``, because this syntax has already
678
705
been removed in Python 3.13.
679
706
0 commit comments