@@ -81,9 +81,9 @@ arbitrarily complex types. For example, you can define nested
8181``TypedDict ``\s and containers with ``TypedDict `` items.
8282Unlike most other types, mypy uses structural compatibility checking
8383(or structural subtyping) with ``TypedDict ``\s . A ``TypedDict `` object with
84- extra items is compatible with (a subtype of) a narrower
84+ extra items can be compatible with (a subtype of) a narrower
8585``TypedDict ``, assuming item types are compatible (*totality * also affects
86- subtyping, as discussed below).
86+ subtyping, as does * closing *, as discussed below).
8787
8888A ``TypedDict `` object is not a subtype of the regular ``dict[...] ``
8989type (and vice versa), since :py:class: `dict ` allows arbitrary keys to be
@@ -276,6 +276,84 @@ vary :ref:`covariantly <variance-of-generics>`:
276276 m: Movie = {" name" : " Jaws" , " year" : 1975 }
277277 process_entry(m) # OK
278278
279+ You can override a read-only item with a compatible subtype, make a
280+ read-only item mutable, and inherit from multiple parents with compatible
281+ definitions:
282+
283+ .. code-block :: python
284+
285+ from collections.abc import Collection, Sequence
286+
287+ class Competition (TypedDict ):
288+ hosts: ReadOnly[Collection[str ]]
289+ entries: ReadOnly[Sequence[Entry]]
290+
291+ class MovieShow (TypedDict ):
292+ entries: list[Movie]
293+
294+ class Oscars (Competition , MovieShow ):
295+ hosts: set[str ]
296+
297+ Defining ``hosts `` as a mutable ``set[str] `` item works as this is compatible
298+ with the read-only ``Collection[str] `` definition in ``Competition ``.
299+ ``entries `` will be of type ``list[Movie] ``, taken from the ``MovieShow `` type,
300+ as it is the only non-readonly definition, and is compatible with the definition
301+ in ``Competition ``.
302+
303+ If an item is only defined in supertypes, and is always read-only, mypy takes
304+ the definition from the first parent in the inheritance order, and raises an
305+ error if any other parent definition is incompatible:
306+
307+ .. code-block :: python
308+
309+ class NameIds (TypedDict ):
310+ ids: ReadOnly[Collection[str ]]
311+
312+ class OrderedIds (TypedDict ):
313+ ids: ReadOnly[Sequence[int | str ]]
314+
315+ class OrderedNameIds (NameIds , OrderedIds ):
316+ pass # Error! Parent definitions incompatible
317+
318+ In this example, the definition of ``ids `` will be taken from ``NameIds ``,
319+ which would not be compatible with the definition in ``OrderedIds ``; reordering
320+ the parents would not solve the problem. Instead, you will need to make a
321+ compatible definition explicitly:
322+
323+ .. code-block :: python
324+
325+ class OrderedNameIds (NameIds , OrderedIds ):
326+ ids: ReadOnly[Sequence[str ]]
327+
328+ Closing
329+ -------
330+
331+ You can use the ``closed `` keyword, introduced to ``TypedDict `` in Python
332+ 3.15 (and available via ``typing_extensions.TypedDict `` in older
333+ versions) to prevent structural subtypes from adding extra keys to a
334+ type (:pep: `728 `):
335+
336+ .. code-block :: python
337+
338+ HasName = TypedDict(" HasName" , {" name" : str })
339+ HasOnlyName = TypedDict(" HasOnlyName" , {" name" : str }, closed = True )
340+ Movie = TypedDict(" Movie" , {" name" : str , " year" : int })
341+
342+ movie: Movie = {" name" : " Nimona" , " year" : 2023 }
343+ has_name: HasName = movie # OK: type is open
344+ has_only_name: HasOnlyName = movie # Error: type is closed
345+
346+ This allows the typechecker to determine that certain operations are safe,
347+ when they otherwise wouldn't be due to the potential presence of unknown
348+ keys.
349+
350+ The ``closed `` keyword can also be used in class-based syntax:
351+
352+ .. code-block :: python
353+
354+ class HasOnlyName (TypedDict , closed = True ):
355+ name: str
356+
279357 Unions of TypedDicts
280358--------------------
281359
@@ -289,6 +367,40 @@ need to give each TypedDict the same key where each value has a unique
289367:ref: `Literal type <literal_types >`. Then, check that key to distinguish
290368between your TypedDicts.
291369
370+ Alternatively, you can implement tagged unions with single-key wrapper dictionaries:
371+
372+ .. code-block :: python
373+
374+ class Book (TypedDict ):
375+ name: str
376+ length: int
377+ ...
378+
379+ class DVD (TypedDict ):
380+ name: str
381+ length: int
382+ ...
383+
384+ TaggedBook = TypedDict(' TaggedBook' , {' book' : Book}, closed = True )
385+ TaggedDVD = TypedDict(' TaggedDVD' , {' dvd' : DVD }, closed = True )
386+ type Inventory = TaggedBook | TaggedDVD
387+
388+ def print_length (inventory : Inventory) -> None :
389+ if " book" in inventory:
390+ print (inventory[" book" ][" length" ], ' pages' )
391+ else :
392+ print (inventory[" dvd" ][" length" ], ' minutes' )
393+
394+ Here, the ``closed `` keyword is necessary to allow the ``if `` guard to safely
395+ narrow the types; without it, there could be a structural subtype of ``TaggedDVD ``
396+ that contains a ``book `` field of arbitrary type.
397+
398+ .. note ::
399+
400+ Applying ``@final `` to a TypedDict is a legacy way of marking it as closed
401+ for the purposes of type narrowing. It was never fully implemented and is
402+ now superseded; it may be removed in future.
403+
292404Inline TypedDict types
293405----------------------
294406
0 commit comments