8000 PEP 728: Address feedback from Carl (#4393) · python/peps@202b0aa · GitHub
[go: up one dir, main page]

Skip to content

Commit 202b0aa

Browse files
PIG208carljm
andauthored
PEP 728: Address feedback from Carl (#4393)
* Address feedback from Carl Discussion: https://discuss.python.org/t/pep-728-typeddict-with-typed-extra-items/45443/153 * Address review feedback * Update peps/pep-0728.rst --------- Co-authored-by: Carl Meyer <carl@oddbird.net>
1 parent b0cc506 commit 202b0aa

File tree

1 file changed

+54
-31
lines changed

1 file changed

+54
-31
lines changed

peps/pep-0728.rst

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ The current behavior of TypedDict prevents users from defining a
3636
TypedDict type when it is expected that the type contains no extra items.
3737

3838
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
4141
`defining a closed TypedDict type <https://github.com/python/mypy/issues/7981>`__.
4242

4343
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``.
126126
Rationale
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

147145
We propose to add a class parameter ``extra_items`` to TypedDict.
148146
It 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

520519
When ``extra_items`` is specified to be read-only on a TypedDict type, it is
521520
possible 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-
612608
Operations that already apply to ``NotRequired`` items should generally also
613609
apply 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

624621
Interaction with Mapping[str, VT]
625622
---------------------------------
@@ -628,8 +625,8 @@ A TypedDict type is :term:`typing:assignable` to a type of the form ``Mapping[st
628625
when all value types of the items in the TypedDict
629626
are assignable to ``VT``. For the purpose of this rule, a
630627
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
633630
<https://typing.python.org/en/latest/spec/typeddict.html#assignability>`__.
634631

635632
For 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

657668
Interaction 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,
697717
because 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

705728
Runtime behavior
706729
----------------

0 commit comments

Comments
 (0)
0