@@ -36,8 +36,8 @@ The current behavior of TypedDict prevents users from defining a
36
36
TypedDict type when it is expected that the type contains no extra items.
37
37
38
38
Due 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
41
41
`defining a closed TypedDict type <https://github.com/python/mypy/issues/7981 >`__.
42
42
43
43
Another 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``.
126
126
Rationale
127
127
=========
128
128
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.
131
130
132
131
`Index Signatures
133
132
<https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures> `__
134
- in TypeScript achieve this:
133
+ in TypeScript allow this:
135
134
136
135
.. code-block :: typescript
137
136
@@ -140,9 +139,8 @@ in TypeScript achieve this:
140
139
[key : string ]: string
141
140
}
142
141
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.
146
144
147
145
We propose to add a class parameter ``extra_items `` to TypedDict.
148
146
It accepts a :term: `typing:type expression ` as the argument; when it is present,
@@ -510,12 +508,13 @@ checks::
510
508
511
509
details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
512
510
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'
514
512
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:
517
515
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 ``.
519
518
520
519
When ``extra_items `` is specified to be read-only on a TypedDict type, it is
521
520
possible for an item to have a :term: `narrower <typing:narrow> ` type than the
@@ -606,9 +605,6 @@ still holds true.
606
605
Operations with arbitrary str keys (instead of string literals or other
607
606
expressions with known string values) should generally be rejected.
608
607
609
- This means that indexed accesses and assignments with arbitrary keys can still
610
- be rejected even when ``extra_items `` is specified.
611
-
612
608
Operations that already apply to ``NotRequired `` items should generally also
613
609
apply to extra items, following the same rationale from the `typing spec
614
610
<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
617
613
cases potentially unsafe operations may be accepted if the alternative is to
618
614
generate false positive errors for idiomatic code.
619
615
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.
623
620
624
621
Interaction with Mapping[str, VT]
625
622
---------------------------------
@@ -628,8 +625,8 @@ A TypedDict type is :term:`typing:assignable` to a type of the form ``Mapping[st
628
625
when all value types of the items in the TypedDict
629
626
are assignable to ``VT ``. For the purpose of this rule, a
630
627
TypedDict 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
633
630
<https://typing.python.org/en/latest/spec/typeddict.html#assignability> `__.
634
631
635
632
For example::
@@ -647,12 +644,26 @@ For example::
647
644
int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
648
645
int_str_mapping: Mapping[str, int | str] = extra_int # OK
649
646
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 :
652
662
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.
656
667
657
668
Interaction with dict[str, VT]
658
669
------------------------------
@@ -687,20 +698,32 @@ For example::
687
698
regular_dict: dict[str, int] = not_required_num_dict # OK
688
699
f(not_required_num_dict) # OK
689
700
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 ``)::
691
704
692
- not_required_num .clear() # OK
705
+ not_required_num_dict .clear() # OK
693
706
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]'
695
708
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,
697
717
because such dict can be a subtype of dict::
698
718
699
719
class CustomDict(dict[str, int]):
700
720
pass
701
721
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)
704
727
705
728
Runtime behavior
706
729
----------------
0 commit comments