Skip to content

Commit 7eecbad

Browse files
Merge branch 'main' into c-thread-pep
2 parents dbe1580 + 45e6128 commit 7eecbad

4 files changed

Lines changed: 421 additions & 193 deletions

File tree

peps/pep-0728.rst

Lines changed: 90 additions & 35 deletions
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,19 @@ 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. The TypedDict should allow non-required extra items of value type
244+
``ReadOnly[object]`` during inheritance or assignability checks, to
245+
preserve the default TypedDict behavior. Extra keys included in TypedDict
246+
object construction should still be caught, as mentioned in TypedDict's
247+
`typing spec
248+
<https://typing.python.org/en/latest/spec/typeddict.html#supported-and-unsupported-operations.>`__.
249+
245250
When ``closed=True`` is set, no extra items are allowed. This is equivalent to
246251
``extra_items=Never``, because there can't be a value type that is assignable to
247252
:class:`~typing.Never`. It is a runtime error to use the ``closed`` and
@@ -275,8 +280,11 @@ child class is also closed::
275280

276281
As a consequence of ``closed=True`` being equivalent to ``extra_items=Never``,
277282
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::
283+
``closed=True``. While they both have the same effect, ``closed=True`` is
284+
preferred over ``extra_items=Never``.
285+
286+
It is possible to use ``closed=True`` when subclassing if the ``extra_items``
287+
argument is a read-only type::
280288

281289
class Movie(TypedDict, extra_items=ReadOnly[str]):
282290
pass
@@ -290,11 +298,6 @@ the same rules that apply to ``extra_items=Never`` also apply to
290298
This will be further discussed in
291299
:ref:`a later section <pep728-inheritance-read-only>`.
292300

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.
297-
298301
``closed`` is also supported with the functional syntax::
299302

300303
Movie = TypedDict("Movie", {"name": str}, closed=True)
@@ -326,13 +329,21 @@ For type checking purposes, ``Unpack[SomeTypedDict]`` with extra items should be
326329
treated as its equivalent in regular parameters, and the existing rules for
327330
function parameters still apply::
328331

329-
class Movie(TypedDict, extra_items=int):
332+
class MovieNoExtra(TypedDict):
333+
name: str
334+
335+
class MovieExtra(TypedDict, extra_items=int):
330336
name: str
331337

332-
def f(**kwargs: Unpack[Movie]) -> None: ...
338+
def f(**kwargs: Unpack[MovieNoExtra]) -> None: ...
339+
def g(**kwargs: Unpack[MovieExtra]) -> None: ...
333340

334341
# Should be equivalent to:
335-
def f(*, name: str, **kwargs: int) -> None: ...
342+
def f(*, name: str) -> None: ...
343+
def g(*, name: str, **kwargs: int) -> None: ...
344+
345+
f(name="No Country for Old Men", year=2007) # Not OK. Unrecognized item
346+
g(name="No Country for Old Men", year=2007) # OK
336347

337348
Interaction with Read-only Items
338349
--------------------------------
@@ -585,8 +596,33 @@ arguments of this type when constructed by calling the class object::
585596
year=2007,
586597
) # Not OK. Extra items not allowed
587598

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

591627
A TypedDict type is :term:`typing:assignable` to a type of the form ``Mapping[str, VT]``
592628
when all value types of the items in the TypedDict
@@ -618,12 +654,12 @@ and ``items()`` on such TypedDict types::
618654
reveal_type(movie.items()) # Revealed type is 'dict_items[str, str]'
619655
reveal_type(movie.values()) # Revealed type is 'dict_values[str, str]'
620656

621-
Interaction with dict[KT, VT]
622-
-----------------------------
657+
Interaction with dict[str, VT]
658+
------------------------------
623659

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

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

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

714767
Use a Special ``__extra_items__`` Key with the ``closed`` Class Parameter
715768
-------------------------------------------------------------------------
@@ -725,7 +778,7 @@ where ``closed=True`` is required for ``__extra_items__`` to be treated
725778
specially, to avoid key collision.
726779

727780
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
781+
Practically, the key collision with a regular key can be mitigated with
729782
workarounds, but since using a reserved key is central to this proposal,
730783
there are limited ways forward to address the concerns.
731784

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

770-
Rejected Ideas
771-
==============
772-
773823
Allowing Extra Items without Specifying the Type
774824
------------------------------------------------
775825

@@ -827,19 +877,24 @@ For example:
827877
[index: string]: number | string
828878
}
829879
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``.
880+
While this restriction allows for sound indexed accesses with arbitrary keys,
881+
it comes with usability limitations discussed in `TypeScript's issue tracker
882+
<https://github.com/microsoft/TypeScript/issues/17867>`__.
883+
A suggestion was to allow excluding the defined keys from the index signature,
884+
to define a type like ``MovieWithExtraNumber``. This probably involves
885+
subtraction types, which is beyond the scope of this PEP.
835886

836887
Reference Implementation
837888
========================
838889

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
890+
This is supported in `pyright 1.1.386
891+
<https://github.com/microsoft/pyright/releases/tag/1.1.386>`_, and an earlier
892+
revision is supported in `pyanalyze
841893
0.12.0 <https://github.com/quora/pyanalyze/releases/tag/v0.12.0>`_.
842894

895+
This is also supported in `typing-extensions 4.13.0
896+
<https://pypi.org/project/typing-extensions/4.13.0/>`_.
897+
843898
Acknowledgments
844899
===============
845900

0 commit comments

Comments
 (0)