Skip to content

Commit a8a31fe

Browse files
authored
PEP 728: Incorporate feedback since last revision (#4380)
* PEP 728: Incorporate discussions since last revision - Mention indexed accesses and assignments with arbitrary keys, per Carl's feedback. - Prepare for submission by clearing the "open issues" sections, with edits when needed. - Include a reference to a prior discussion on `@final` addressing Victorien feedback (https://discuss.python.org/t/pep-728-typeddict-with-typed-extra-items/45443/138). - Update reference implementations. Signed-off-by: Zixuan James Li <[email protected]> * Address follow-ups Signed-off-by: Zixuan James Li <[email protected]> * Address feedback from Andrej Also comments from discussion thread: https://discuss.python.org/t/pep-728-typeddict-with-typed-extra-items/45443/148 --------- Signed-off-by: Zixuan James Li <[email protected]>
1 parent be2ab53 commit a8a31fe

File tree

1 file changed

+80
-31
lines changed

1 file changed

+80
-31
lines changed

peps/pep-0728.rst

+80-31
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@ determining `supported and unsupported operations
187187
The ``extra_items`` Class Parameter
188188
-----------------------------------
189189

190-
For a TypedDict type that specifies ``extra_items``, during construction, the
191-
value type of each unknown item is expected to be non-required and assignable
192-
to the ``extra_items`` argument. For example::
190+
By default ``extra_items`` is unset. For a TypedDict type that specifies
191+
``extra_items``, during construction, the value type of each unknown item
192+
is expected to be non-required and assignable to the ``extra_items`` argument.
193+
For example::
193194

194195
class Movie(TypedDict, extra_items=bool):
195196
name: str
@@ -233,15 +234,14 @@ Here, ``'year'`` in ``a`` is an extra key defined on ``Movie`` whose value type
233234
is ``int``. ``'other_extra_key'`` in ``b`` is another extra key whose value type
234235
must be assignable to the value of ``extra_items`` defined on ``MovieBase``.
235236

236-
``extra_items`` is also supported with the functional syntax::
237-
238-
Movie = TypedDict("Movie", {"name": str}, extra_items=int | None)
239-
240237
.. _typed-dict-closed:
241238

242239
The ``closed`` Class Parameter
243240
------------------------------
244241

242+
When neither ``extra_items`` nor ``closed=True`` is specified, ``closed=False``
243+
is assumed.
244+
245245
When ``closed=True`` is set, no extra items are allowed. This is equivalent to
246246
``extra_items=Never``, because there can't be a value type that is assignable to
247247
:class:`~typing.Never`. It is a runtime error to use the ``closed`` and
@@ -275,8 +275,11 @@ child class is also closed::
275275

276276
As a consequence of ``closed=True`` being equivalent to ``extra_items=Never``,
277277
the same rules that apply to ``extra_items=Never`` also apply to
278-
``closed=True``. It is possible to use ``closed=True`` when subclassing if the
279-
``extra_items`` argument is a read-only type::
278+
``closed=True``. While they both have the same effect, ``closed=True`` is
279+
preferred over ``extra_items=Never``.
280+
281+
It is possible to use ``closed=True`` when subclassing if the ``extra_items``
282+
argument is a read-only type::
280283

281284
class Movie(TypedDict, extra_items=ReadOnly[str]):
282285
pass
@@ -290,10 +293,12 @@ the same rules that apply to ``extra_items=Never`` also apply to
290293
This will be further discussed in
291294
:ref:`a later section <pep728-inheritance-read-only>`.
292295

293-
When neither ``extra_items`` nor ``closed=True`` is specified, the TypedDict
294-
is assumed to allow non-required extra items of value type ``ReadOnly[object]``
295-
during inheritance or assignability checks. This preserves the existing behavior
296-
of TypedDict.
296+
The TypedDict should allow non-required extra items of value type
297+
``ReadOnly[object]`` during inheritance or assignability checks, to
298+
preserve the default TypedDict behavior. Extra keys included in TypedDict
299+
object construction should still be caught, as mentioned in TypedDict's
300+
`typing spec
301+
<https://typing.python.org/en/latest/spec/typeddict.html#supported-and-unsupported-operations.>`__.
297302

298303
``closed`` is also supported with the functional syntax::
299304

@@ -585,8 +590,33 @@ arguments of this type when constructed by calling the class object::
585590
year=2007,
586591
) # Not OK. Extra items not allowed
587592

588-
Interaction with Mapping[KT, VT]
589-
--------------------------------
593+
Supported and Unsupported Operations
594+
------------------------------------
595+
596+
This statement from the `typing spec
597+
<https://typing.python.org/en/latest/spec/typeddict.html#supported-and-unsupported-operations>`__
598+
still holds true.
599+
600+
Operations with arbitrary str keys (instead of string literals or other
601+
expressions with known string values) should generally be rejected.
602+
603+
This means that indexed accesses and assignments with arbitrary keys can still
604+
be rejected even when ``extra_items`` is specified.
605+
606+
Operations that already apply to ``NotRequired`` items should generally also
607+
apply to extra items, following the same rationale from the `typing spec
608+
<https://typing.python.org/en/latest/spec/typeddict.html#supported-and-unsupported-operations>`__:
609+
610+
The exact type checking rules are up to each type checker to decide. In some
611+
cases potentially unsafe operations may be accepted if the alternative is to
612+
generate false positive errors for idiomatic code.
613+
614+
Some operations are allowed due to the TypedDict being
615+
:term:`typing:assignable` to ``Mapping[str, VT]`` or ``dict[str, VT]``.
616+
The two following sections will expand on that.
617+
618+
Interaction with Mapping[str, VT]
619+
---------------------------------
590620

591621
A TypedDict type is :term:`typing:assignable` to a type of the form ``Mapping[str, VT]``
592622
when all value types of the items in the TypedDict
@@ -618,12 +648,12 @@ and ``items()`` on such TypedDict types::
618648
reveal_type(movie.items()) # Revealed type is 'dict_items[str, str]'
619649
reveal_type(movie.values()) # Revealed type is 'dict_values[str, str]'
620650

621-
Interaction with dict[KT, VT]
622-
-----------------------------
651+
Interaction with dict[str, VT]
652+
------------------------------
623653

624654
Because the presence of ``extra_items`` on a closed TypedDict type
625655
prohibits additional required keys in its :term:`typing:structural`
626-
:term:`typing:subtypes <subtype>`, we can determine if the TypedDict type and
656+
:term:`subtypes <subtype>`, we can determine if the TypedDict type and
627657
its structural subtypes will ever have any required key during static analysis.
628658

629659
The TypedDict type is :term:`typing:assignable` to ``dict[str, VT]`` if all
@@ -708,8 +738,25 @@ been removed in Python 3.13.
708738
Because this is a type-checking feature, it can be made available to older
709739
versions as long as the type checker supports it.
710740

711-
Open Issues
712-
===========
741+
Rejected Ideas
742+
==============
743+
744+
Use ``@final`` instead of ``closed`` Class Parameter
745+
-----------------------------------------------------
746+
747+
This was discussed `here <https://github.com/python/mypy/issues/7981>`__.
748+
749+
Quoting a relevant `comment
750+
<https://github.com/python/mypy/issues/7981#issuecomment-2080161813>`__
751+
from Eric Traut:
752+
753+
The @final class decorator indicates that a class cannot be subclassed. This
754+
makes sense for classes that define nominal types. However, TypedDict is a
755+
structural type, similar to a Protocol. That means two TypedDict classes
756+
with different names but the same field definitions are equivalent types.
757+
Their names and hierarchies don't matter for determining type consistency.
758+
For that reason, @final has no impact on a TypedDict type consistency rules,
759+
nor should it change the behavior of items or values.
713760

714761
Use a Special ``__extra_items__`` Key with the ``closed`` Class Parameter
715762
-------------------------------------------------------------------------
@@ -725,7 +772,7 @@ where ``closed=True`` is required for ``__extra_items__`` to be treated
725772
specially, to avoid key collision.
726773

727774
Some members of the community concern about the elegance of the syntax.
728-
Practiaclly, the key collision with a regular key can be mitigated with
775+
Practically, the key collision with a regular key can be mitigated with
729776
workarounds, but since using a reserved key is central to this proposal,
730777
there are limited ways forward to address the concerns.
731778

@@ -767,9 +814,6 @@ types altogether, but there are some disadvantages. `For example
767814
- The types don't appear in an annotation context, so their evaluation will
768815
not be deferred.
769816

770-
Rejected Ideas
771-
==============
772-
773817
Allowing Extra Items without Specifying the Type
774818
------------------------------------------------
775819

@@ -827,19 +871,24 @@ For example:
827871
[index: string]: number | string
828872
}
829873
830-
This is a known limitation discussed in `TypeScript's issue tracker
831-
<https://github.com/microsoft/TypeScript/issues/17867>`__,
832-
where it is suggested that there should be a way to exclude the defined keys
833-
from the index signature so that it is possible to define a type like
834-
``MovieWithExtraNumber``.
874+
While this restriction allows for sound indexed accesses with arbitrary keys,
875+
it comes with usability limitations discussed in `TypeScript's issue tracker
876+
<https://github.com/microsoft/TypeScript/issues/17867>`__.
877+
A suggestion was to allow excluding the defined keys from the index signature,
878+
to define a type like ``MovieWithExtraNumber``. This probably involves
879+
subtraction types, which is beyond the scope of this PEP.
835880

836881
Reference Implementation
837882
========================
838883

839-
An earlier revision of proposal is supported in `pyright 1.1.352
840-
<https://github.com/microsoft/pyright/releases/tag/1.1.352>`_, and `pyanalyze
884+
This is supported in `pyright 1.1.386
885+
<https://github.com/microsoft/pyright/releases/tag/1.1.386>`_, and an earlier
886+
revision is supported in `pyanalyze
841887
0.12.0 <https://github.com/quora/pyanalyze/releases/tag/v0.12.0>`_.
842888

889+
This is also supported in `typing-extensions 4.13.0
890+
<https://pypi.org/project/typing-extensions/4.13.0/>`_.
891+
843892
Acknowledgments
844893
===============
845894

0 commit comments

Comments
 (0)