@@ -36,8 +36,8 @@ The current behavior of TypedDict prevents users from defining a
3636TypedDict type when it is expected that the type contains no extra items.
3737
3838Due to the possible presence of extra items, type checkers cannot infer more
39- precise return types for ``.items() `` and ``.values() `` on a TypedDict. This can
40- also be resolved by
39+ precise return types for ``.items() `` and ``.values() `` on a TypedDict.
40+ This can be resolved by
4141`defining a closed TypedDict type <https://github.com/python/mypy/issues/7981 >`__.
4242
4343Another possible use case for this is a sound way to
@@ -126,12 +126,11 @@ that the old typing behavior can be supported in combination with ``Unpack``.
126126Rationale
127127=========
128128
129- A type that allows extra items of type ``str `` on a TypedDict can be loosely
130- described as the intersection between the TypedDict and ``Mapping[str, str] ``.
129+ Suppose we want a type that allows extra items of type ``str `` on a TypedDict.
131130
132131`Index Signatures
133132<https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures> `__
134- in TypeScript achieve this:
133+ in TypeScript allow this:
135134
136135.. code-block :: typescript
137136
@@ -140,9 +139,8 @@ in TypeScript achieve this:
140139 [key : string ]: string
141140 }
142141
143- This proposal aims to support a similar feature without introducing general
144- intersection of types or syntax changes, offering a natural extension to the
145- existing assignability rules.
142+ This proposal aims to support a similar feature without syntax changes,
143+ offering a natural extension to the existing assignability rules.
146144
147145We propose to add a class parameter ``extra_items `` to TypedDict.
148146It accepts a :term: `typing:type expression ` as the argument; when it is present,
@@ -510,12 +508,13 @@ checks::
510508
511509 details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
512510 movie: Movie = details # Not OK. 'year' is not required in 'Movie',
513- # so it shouldn't be required in 'MovieWithYear' either
511+ # but it is required in 'MovieWithYear'
514512
515- Because ``'year' `` is absent in ``Movie ``, `` extra_items `` is considered the
516- corresponding key. `` 'year' `` being required violates this rule:
513+ where ``MovieWithYear `` (B) is not assignable to ``Movie `` (A)
514+ according to this rule:
517515
518- * For each required key in ``A ``, the corresponding key is required in ``B ``.
516+ * For each non-required key in ``A ``, if the item is not read-only in ``A ``,
517+ the corresponding key is not required in ``B ``.
519518
520519When ``extra_items `` is specified to be read-only on a TypedDict type, it is
521520possible for an item to have a :term: `narrower <typing:narrow> ` type than the
@@ -606,9 +605,6 @@ still holds true.
606605 Operations with arbitrary str keys (instead of string literals or other
607606 expressions with known string values) should generally be rejected.
608607
609- This means that indexed accesses and assignments with arbitrary keys can still
610- be rejected even when ``extra_items `` is specified.
611-
612608Operations that already apply to ``NotRequired `` items should generally also
613609apply to extra items, following the same rationale from the `typing spec
614610<https://typing.python.org/en/latest/spec/typeddict.html#supported-and-unsupported-operations> `__:
@@ -617,9 +613,10 @@ apply to extra items, following the same rationale from the `typing spec
617613 cases potentially unsafe operations may be accepted if the alternative is to
618614 generate false positive errors for idiomatic code.
619615
620- Some operations are allowed due to the TypedDict being
621- :term: `typing:assignable ` to ``Mapping[str, VT] `` or ``dict[str, VT] ``.
622- The two following sections will expand on that.
616+ Some operations, including indexed accesses and assignments with arbitrary str keys,
617+ may be allowed due to the TypedDict being :term: `typing:assignable ` to
618+ ``Mapping[str, VT] `` or ``dict[str, VT] ``. The two following sections will expand
619+ on that.
623620
624621Interaction with Mapping[str, VT]
625622---------------------------------
@@ -628,8 +625,8 @@ A TypedDict type is :term:`typing:assignable` to a type of the form ``Mapping[st
628625when all value types of the items in the TypedDict
629626are assignable to ``VT ``. For the purpose of this rule, a
630627TypedDict that does not have ``extra_items= `` or ``closed= `` set is considered
631- to have an item with a value of type ``object ``. This extends the current
632- assignability rule from the `typing spec
628+ to have an item with a value of type ``ReadOnly[ object] ``. This extends the
629+ current assignability rule from the `typing spec
633630<https://typing.python.org/en/latest/spec/typeddict.html#assignability> `__.
634631
635632For example::
@@ -647,12 +644,26 @@ For example::
647644 int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
648645 int_str_mapping: Mapping[str, int | str] = extra_int # OK
649646
650- Type checkers should be able to infer the precise return types of ``values() ``
651- and ``items() `` on such TypedDict types::
647+ Type checkers should infer the precise signatures of ``values() `` and ``items() ``
648+ on such TypedDict types::
649+
650+ def foo(movie: MovieExtraInt) -> None:
651+ reveal_type(movie.items()) # Revealed type is 'dict_items[str, str | int]'
652+ reveal_type(movie.values()) # Revealed type is 'dict_values[str, str | int]'
653+
654+ By extension of this assignability rule, type checkers may allow indexed accesses
655+ with arbitrary str keys when ``extra_items `` or ``closed=True `` is specified.
656+ For example::
657+
658+ def bar(movie: MovieExtraInt, key: str) -> None:
659+ reveal_type(movie[key]) # Revealed type is 'str | int'
660+
661+ .. _pep728-type-narrowing :
652662
653- def fun(movie: MovieExtraStr) -> None:
654- reveal_type(movie.items()) # Revealed type is 'dict_items[str, str]'
655- reveal_type(movie.values()) # Revealed type is 'dict_values[str, str]'
663+ Defining the type narrowing behavior for TypedDict is out-of-scope for this PEP.
664+ This leaves flexibility for a type checker to be more/less restrictive about
665+ indexed accesses with arbitrary str keys. For example, a type checker may opt
666+ for more restriction by requiring an explicit ``'x' in d `` check.
656667
657668Interaction with dict[str, VT]
658669------------------------------
@@ -687,20 +698,32 @@ For example::
687698 regular_dict: dict[str, int] = not_required_num_dict # OK
688699 f(not_required_num_dict) # OK
689700
690- In this case, methods that are previously unavailable on a TypedDict are allowed::
701+ In this case, methods that are previously unavailable on a TypedDict are allowed,
702+ with signatures matching ``dict[str, VT] ``
703+ (e.g.: ``__setitem__(self, key: str, value: VT) -> None ``)::
691704
692- not_required_num .clear() # OK
705+ not_required_num_dict .clear() # OK
693706
694- reveal_type(not_required_num .popitem()) # OK. Revealed type is tuple[str, int]
707+ reveal_type(not_required_num_dict .popitem()) # OK. Revealed type is ' tuple[str, int]'
695708
696- However, ``dict[str, VT] `` is not necessarily assignable to a TypedDict type,
709+ def f(not_required_num_dict: IntDictWithNum, key: str):
710+ not_required_num_dict[key] = 42 # OK
711+ del not_required_num_dict[key] # OK
712+
713+ :ref: `Notes on indexed accesses <pep728-type-narrowing >` from the previous section
714+ still apply.
715+
716+ ``dict[str, VT] `` is not assignable to a TypedDict type,
697717because such dict can be a subtype of dict::
698718
699719 class CustomDict(dict[str, int]):
700720 pass
701721
702- not_a_regular_dict: CustomDict = {"num": 1}
703- int_dict: IntDict = not_a_regular_dict # Not OK
722+ def f(might_not_be_a_builtin_dict: dict[str, int]):
723+ int_dict: IntDict = might_not_be_a_builtin_dict # Not OK
724+
725+ not_a_builtin_dict: CustomDict = {"num": 1}
726+ f(not_a_builtin_dict)
704727
705728Runtime behavior
706729----------------
0 commit comments