2024-02-08 21:55:10 -05:00
|
|
|
PEP: 728
|
|
|
|
Title: TypedDict with Typed Extra Items
|
|
|
|
Author: Zixuan James Li <p359101898@gmail.com>
|
|
|
|
Sponsor: Jelle Zijlstra <jelle.zijlstra@gmail.com>
|
2024-02-09 00:31:58 -05:00
|
|
|
Discussions-To: https://discuss.python.org/t/pep-728-typeddict-with-typed-extra-items/45443
|
2024-02-08 21:55:10 -05:00
|
|
|
Status: Draft
|
|
|
|
Type: Standards Track
|
|
|
|
Topic: Typing
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
Created: 12-Sep-2023
|
2024-10-18 22:14:30 -04:00
|
|
|
Python-Version: 3.14
|
2024-02-09 00:31:58 -05:00
|
|
|
Post-History: `09-Feb-2024 <https://discuss.python.org/t/pep-728-typeddict-with-typed-extra-items/45443>`__,
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
========
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
This PEP adds two class parameters, ``closed`` and ``extra_items``
|
|
|
|
to type the extra items on a :class:`~typing.TypedDict`. This addresses the
|
|
|
|
need to define closed TypedDict types or to type a subset of keys that might
|
|
|
|
appear in a ``dict`` while permitting additional items of a specified type.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Motivation
|
|
|
|
==========
|
|
|
|
|
|
|
|
A :py:class:`typing.TypedDict` type can annotate the value type of each known
|
2024-10-18 22:14:30 -04:00
|
|
|
item in a dictionary. However, due to :term:`typing:structural`
|
|
|
|
:term:`assignability <typing:assignable>`, a TypedDict can have extra items
|
|
|
|
that are not visible through its type. There is currently no way to restrict
|
|
|
|
the types of items that might be present in the TypedDict type's
|
|
|
|
:term:`consistent subtypes <typing:consistent subtype>`.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Disallowing Extra Items Explicitly
|
|
|
|
----------------------------------
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
The current behavior of TypedDict prevents users from defining a
|
|
|
|
TypedDict type when it is expected that the type contains no extra items.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Due to the possible presence of extra items, type checkers cannot infer more
|
|
|
|
precise return types for ``.items()`` and ``.values()`` on a TypedDict. This can
|
|
|
|
also be resolved by
|
|
|
|
`defining a closed TypedDict type <https://github.com/python/mypy/issues/7981>`__.
|
|
|
|
|
|
|
|
Another possible use case for this is a sound way to
|
|
|
|
`enable type narrowing <https://github.com/python/mypy/issues/9953>`__ with the
|
|
|
|
``in`` check::
|
|
|
|
|
|
|
|
class Movie(TypedDict):
|
|
|
|
name: str
|
|
|
|
director: str
|
|
|
|
|
|
|
|
class Book(TypedDict):
|
|
|
|
name: str
|
|
|
|
author: str
|
|
|
|
|
|
|
|
def fun(entry: Movie | Book) -> None:
|
|
|
|
if "author" in entry:
|
2024-10-18 22:14:30 -04:00
|
|
|
reveal_type(entry) # Revealed type is still 'Movie | Book'
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Nothing prevents a ``dict`` that is assignable with ``Movie`` to have the
|
|
|
|
``author`` key, and under the current specification it would be incorrect for
|
|
|
|
the type checker to :term:`typing:narrow` its type.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Allowing Extra Items of a Certain Type
|
|
|
|
--------------------------------------
|
|
|
|
|
|
|
|
For supporting API interfaces or legacy codebase where only a subset of possible
|
2024-10-18 22:14:30 -04:00
|
|
|
keys are known, it would be useful to explicitly specify extra items of certain
|
|
|
|
value types.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
However, the typing spec is more restrictive when checking the construction of a
|
2024-02-08 21:55:10 -05:00
|
|
|
TypedDict, `preventing users <https://github.com/python/mypy/issues/4617>`__
|
|
|
|
from doing this::
|
|
|
|
|
|
|
|
class MovieBase(TypedDict):
|
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
def foo(movie: MovieBase) -> None:
|
2024-02-08 21:55:10 -05:00
|
|
|
# movie can have extra items that are not visible through MovieBase
|
|
|
|
...
|
|
|
|
|
|
|
|
movie: MovieBase = {"name": "Blade Runner", "year": 1982} # Not OK
|
2024-10-18 22:14:30 -04:00
|
|
|
foo({"name": "Blade Runner", "year": 1982}) # Not OK
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
While the restriction is enforced when constructing a TypedDict, due to
|
2024-10-18 22:14:30 -04:00
|
|
|
:term:`typing:structural` :term:`assignability <typing:assignable>`, the
|
|
|
|
TypedDict may have extra items that are not visible through its type.
|
|
|
|
For example::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
class Movie(MovieBase):
|
|
|
|
year: int
|
|
|
|
|
|
|
|
movie: Movie = {"name": "Blade Runner", "year": 1982}
|
2024-10-18 22:14:30 -04:00
|
|
|
foo(movie) # OK
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
It is not possible to acknowledge the existence of the extra items through
|
|
|
|
``in`` checks and access them without breaking type safety, even though they
|
2024-10-18 22:14:30 -04:00
|
|
|
might exist from some :term:`consistent subtypes <typing:consistent subtype>` of
|
|
|
|
``MovieBase``::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
def bar(movie: MovieBase) -> None:
|
2024-02-08 21:55:10 -05:00
|
|
|
if "year" in movie:
|
|
|
|
reveal_type(movie["year"]) # Error: TypedDict 'MovieBase' has no key 'year'
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Some workarounds have already been implemented to allow
|
|
|
|
extra items, but none of them is ideal. For mypy,
|
2024-02-08 21:55:10 -05:00
|
|
|
``--disable-error-code=typeddict-unknown-key``
|
|
|
|
`suppresses type checking error <https://github.com/python/mypy/pull/14225>`__
|
|
|
|
specifically for unknown keys on TypedDict. This sacrifices type safety over
|
|
|
|
flexibility, and it does not offer a way to specify that the TypedDict type
|
2024-10-18 22:14:30 -04:00
|
|
|
expects additional keys whose value types are assignable with a certain type.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Support Additional Keys for ``Unpack``
|
|
|
|
--------------------------------------
|
|
|
|
|
|
|
|
:pep:`692` adds a way to precisely annotate the types of individual keyword
|
|
|
|
arguments represented by ``**kwargs`` using TypedDict with ``Unpack``. However,
|
|
|
|
because TypedDict cannot be defined to accept arbitrary extra items, it is not
|
2024-10-18 22:14:30 -04:00
|
|
|
possible to `allow additional keyword arguments
|
|
|
|
<https://discuss.python.org/t/pep-692-using-typeddict-for-more-precise-kwargs-typing/17314/87>`__
|
2024-02-08 21:55:10 -05:00
|
|
|
that are not known at the time the TypedDict is defined.
|
|
|
|
|
|
|
|
Given the usage of pre-:pep:`692` type annotation for ``**kwargs`` in existing
|
|
|
|
codebases, it will be valuable to accept and type extra items on TypedDict so
|
2024-10-18 22:14:30 -04:00
|
|
|
that the old typing behavior can be supported in combination with ``Unpack``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Rationale
|
|
|
|
=========
|
|
|
|
|
|
|
|
A type that allows extra items of type ``str`` on a TypedDict can be loosely
|
|
|
|
described as the intersection between the TypedDict and ``Mapping[str, str]``.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
`Index Signatures
|
|
|
|
<https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures>`__
|
2024-02-08 21:55:10 -05:00
|
|
|
in TypeScript achieve this:
|
|
|
|
|
|
|
|
.. code-block:: typescript
|
|
|
|
|
|
|
|
type Foo = {
|
|
|
|
a: string
|
|
|
|
[key: string]: string
|
|
|
|
}
|
|
|
|
|
|
|
|
This proposal aims to support a similar feature without introducing general
|
|
|
|
intersection of types or syntax changes, offering a natural extension to the
|
2024-10-18 22:14:30 -04:00
|
|
|
existing assignability rules.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
We propose to add a class parameter ``extra_items`` to TypedDict.
|
|
|
|
It accepts a :term:`typing:type expression` as the argument; when it is present,
|
|
|
|
extra items are allowed, and their value types must be assignable to the
|
|
|
|
type expression value.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
An application of this is to disallow extra items. We propose to add a
|
|
|
|
``closed`` class parameter, which only accepts a literal ``True`` or ``False``
|
|
|
|
as the argument. It should be a runtime error when ``closed`` and
|
|
|
|
``extra_items`` are used at the same time.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
|
|
|
Different from index signatures, the types of the known items do not need to be
|
2024-10-18 22:14:30 -04:00
|
|
|
assignable to the ``extra_items`` argument.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
There are some advantages to this approach:
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- We can build on top of the `assignability rules defined in the typing spec
|
|
|
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#assignability>`__,
|
|
|
|
where ``extra_items`` can be treated as a pseudo-item.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
|
|
|
- There is no need to introduce a grammar change to specify the type of the
|
|
|
|
extra items.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- We can precisely type the extra items without requiring the value types of the
|
|
|
|
known items to be :term:`typing:assignable` to ``extra_items``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- We do not lose backwards compatibility as both ``extra_items`` and ``closed``
|
|
|
|
are opt-in only features.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Specification
|
|
|
|
=============
|
|
|
|
|
|
|
|
This specification is structured to parallel :pep:`589` to highlight changes to
|
|
|
|
the original TypedDict specification.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
If ``extra_items`` is specified, extra items are treated as :ref:`non-required
|
|
|
|
<typing:required-notrequired>`
|
|
|
|
items matching the ``extra_items`` argument, whose keys are allowed when
|
|
|
|
determining `supported and unsupported operations
|
2024-02-08 21:55:10 -05:00
|
|
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#supported-and-unsupported-operations>`__.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
The ``extra_items`` Class Parameter
|
|
|
|
-----------------------------------
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
For a TypedDict type that specifies ``extra_items``, during construction, the
|
|
|
|
value type of each unknown item is expected to be non-required and assignable
|
|
|
|
to the ``extra_items`` argument. For example::
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class Movie(TypedDict, extra_items=bool):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
|
|
|
a: Movie = {"name": "Blade Runner", "novel_adaptation": True} # OK
|
|
|
|
b: Movie = {
|
|
|
|
"name": "Blade Runner",
|
2024-10-18 22:14:30 -04:00
|
|
|
"year": 1982, # Not OK. 'int' is not assignable to 'bool'
|
2024-02-08 21:55:10 -05:00
|
|
|
}
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Here, ``extra_items=bool`` specifies that items other than ``'name'``
|
|
|
|
have a value type of ``bool`` and are non-required.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
The alternative inline syntax is also supported::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Movie = TypedDict("Movie", {"name": str}, extra_items=bool)
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Accessing extra items is allowed. Type checkers must infer their value type from
|
|
|
|
the ``extra_items`` argument::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
def f(movie: Movie) -> None:
|
|
|
|
reveal_type(movie["name"]) # Revealed type is 'str'
|
|
|
|
reveal_type(movie["novel_adaptation"]) # Revealed type is 'bool'
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
``extra_items`` is inherited through subclassing::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieBase(TypedDict, extra_items=int | None):
|
2024-02-17 17:31:57 -05:00
|
|
|
name: str
|
|
|
|
|
|
|
|
class Movie(MovieBase):
|
2024-10-18 22:14:30 -04:00
|
|
|
year: int
|
|
|
|
|
|
|
|
a: Movie = {"name": "Blade Runner", "year": None} # Not OK. 'None' is incompatible with 'int'
|
2024-03-14 11:06:14 -04:00
|
|
|
b: Movie = {
|
|
|
|
"name": "Blade Runner",
|
2024-10-18 22:14:30 -04:00
|
|
|
"year": 1982,
|
2024-03-14 11:06:14 -04:00
|
|
|
"other_extra_key": None,
|
|
|
|
} # OK
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Here, ``'year'`` in ``a`` is an extra key defined on ``Movie`` whose value type
|
|
|
|
is ``int``. ``'other_extra_key'`` in ``b`` is another extra key whose value type
|
|
|
|
must be assignable to the value of ``extra_items`` defined on ``MovieBase``.
|
|
|
|
|
|
|
|
The ``closed`` Class Parameter
|
|
|
|
------------------------------
|
|
|
|
|
|
|
|
When ``closed=True`` is set, no extra items are allowed. This is a shorthand for
|
|
|
|
``extra_items=Never``, because there can't be a value type that is assignable to
|
|
|
|
:class:`~typing.Never`.
|
|
|
|
|
|
|
|
Similar to ``total``, only a literal ``True`` or ``False`` is supported as the
|
|
|
|
value of the ``closed`` argument; ``closed`` is ``False`` by default, which
|
|
|
|
preserves the previous TypedDict behavior.
|
|
|
|
|
|
|
|
The value of ``closed`` is not inherited through subclassing, but the
|
|
|
|
implicitly set ``extra_items=Never`` is. It should be an error to use the
|
|
|
|
default ``closed=False`` when subclassing a closed TypedDict type::
|
|
|
|
|
|
|
|
class BaseMovie(TypedDict, closed=True):
|
|
|
|
name: str
|
|
|
|
|
|
|
|
class MovieA(BaseMovie): # Not OK. An explicit 'closed=True' is required
|
|
|
|
pass
|
|
|
|
|
|
|
|
class MovieB(BaseMovie, closed=True): # OK
|
|
|
|
pass
|
|
|
|
|
|
|
|
Setting both ``closed`` and ``extra_items`` when defining a TypedDict type
|
|
|
|
should always be a runtime error::
|
|
|
|
|
|
|
|
class Person(TypedDict, closed=False, extra_items=bool): # Not OK. 'closed' and 'extra_items' are incompatible
|
|
|
|
name: str
|
|
|
|
|
|
|
|
As a consequence of ``closed=True`` being equivalent to ``extra_items=Never``.
|
|
|
|
The same rules that apply to ``extra_items=Never`` should also apply to
|
|
|
|
``closed=True``. It is possible to use ``closed=True`` when subclassing if the
|
|
|
|
``extra_items`` argument is a read-only type::
|
|
|
|
|
|
|
|
class Movie(TypedDict, extra_items=ReadOnly[str]):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class MovieClosed(Movie, closed=True): # OK
|
|
|
|
pass
|
|
|
|
|
|
|
|
class MovieNever(Movie, extra_items=Never): # Not OK. 'closed=True' is preferred
|
|
|
|
pass
|
|
|
|
|
|
|
|
This will be further discussed in
|
|
|
|
:ref:`a later section <pep728-inheritance-read-only>`.
|
|
|
|
|
|
|
|
When neither ``extra_items`` nor ``closed=True`` is specified, the TypedDict
|
|
|
|
is assumed to allow non-required extra items of value type ``ReadOnly[object]``
|
|
|
|
during inheritance or assignability checks. This preserves the existing behavior
|
|
|
|
of TypedDict.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Interaction with Totality
|
|
|
|
-------------------------
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
It is an error to use ``Required[]`` or ``NotRequired[]`` with ``extra_items``.
|
|
|
|
``total=False`` and ``total=True`` have no effect on ``extra_items`` itself.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
The extra items are non-required, regardless of the `totality
|
|
|
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#totality>`__ of the
|
|
|
|
TypedDict. `Operations
|
|
|
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#supported-and-unsupported-operations>`__
|
|
|
|
that are available to ``NotRequired`` items should also be available to the
|
|
|
|
extra items::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class Movie(TypedDict, extra_items=int):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
|
|
|
def f(movie: Movie) -> None:
|
2024-10-18 22:14:30 -04:00
|
|
|
del movie["name"] # Not OK. The value type of 'name' is 'Required[int]'
|
|
|
|
del movie["year"] # OK. The value type of 'year' is 'NotRequired[int]'
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Interaction with ``Unpack``
|
|
|
|
---------------------------
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
For type checking purposes, ``Unpack[SomeTypedDict]`` with extra items should be
|
2024-02-08 21:55:10 -05:00
|
|
|
treated as its equivalent in regular parameters, and the existing rules for
|
|
|
|
function parameters still apply::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class Movie(TypedDict, extra_items=int):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
|
|
|
def f(**kwargs: Unpack[Movie]) -> None: ...
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
# Should be equivalent to:
|
2024-02-08 21:55:10 -05:00
|
|
|
def f(*, name: str, **kwargs: int) -> None: ...
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Interaction with Read-only Items
|
|
|
|
--------------------------------
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
When the ``extra_items`` argument is annotated with the ``ReadOnly[]``
|
|
|
|
:term:`typing:type qualifier`, the extra items on the TypedDict have the
|
|
|
|
properties of read-only items. This interacts with inheritance rules specified
|
|
|
|
in :ref:`Read-only Items <typing:readonly>`.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Notably, if the TypedDict type specifies ``extra_items`` to be read-only,
|
|
|
|
subclasses of the TypedDict type may redeclare ``extra_items``.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
|
|
|
Because a non-closed TypedDict type implicitly allows non-required extra items
|
2024-10-18 22:14:30 -04:00
|
|
|
of value type ``ReadOnly[object]``, its subclass can override the
|
|
|
|
``extra_items`` argument with more specific types.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
|
|
|
More details are discussed in the later sections.
|
|
|
|
|
2024-02-08 21:55:10 -05:00
|
|
|
Inheritance
|
|
|
|
-----------
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
``extra_items`` is inherited in a similar way as a regular ``key: value_type``
|
|
|
|
item. As with the other keys, the `inheritance rules
|
|
|
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#inheritance>`__
|
|
|
|
and :ref:`Read-only Items <typing:readonly>` inheritance rules apply.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
We need to reinterpret these rules to define how ``extra_items`` interacts with
|
|
|
|
them.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
* Changing a field type of a parent TypedDict class in a subclass is not allowed.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
First, it is not allowed to change the value of ``extra_items`` in a subclass
|
2024-02-08 21:55:10 -05:00
|
|
|
unless it is declared to be ``ReadOnly`` in the superclass::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class Parent(TypedDict, extra_items=int | None):
|
|
|
|
pass
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class Child(Parent, extra_items=int): # Not OK. Like any other TypedDict item, extra_items's type cannot be changed
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Second, ``extra_items=T`` effectively defines the value type of any unnamed
|
2024-02-17 17:31:57 -05:00
|
|
|
items accepted to the TypedDict and marks them as non-required. Thus, the above
|
2024-02-08 21:55:10 -05:00
|
|
|
restriction applies to any additional items defined in a subclass. For each item
|
|
|
|
added in a subclass, all of the following conditions should apply:
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
.. _pep728-inheritance-read-only:
|
|
|
|
|
|
|
|
- If ``extra_items`` is read-only
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-09 08:24:13 -05:00
|
|
|
- The item can be either required or non-required
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- The item's value type is :term:`typing:assignable` to ``T``
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- If ``extra_items`` is not read-only
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-09 08:24:13 -05:00
|
|
|
- The item is non-required
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- The item's value type is :term:`typing:consistent` with ``T``
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- If ``extra_items`` is not overriden, the subclass inherits it as-is.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
For example::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieBase(TypedDict, extra_items=int | None):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class AdaptedMovie(MovieBase): # Not OK. 'bool' is not assignable to 'int | None'
|
2024-02-08 21:55:10 -05:00
|
|
|
adapted_from_novel: bool
|
|
|
|
|
|
|
|
class MovieRequiredYear(MovieBase): # Not OK. Required key 'year' is not known to 'Parent'
|
|
|
|
year: int | None
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieNotRequiredYear(MovieBase): # Not OK. 'int | None' is not assignable to 'int'
|
2024-02-08 21:55:10 -05:00
|
|
|
year: NotRequired[int]
|
|
|
|
|
|
|
|
class MovieWithYear(MovieBase): # OK
|
|
|
|
year: NotRequired[int | None]
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class BookBase(TypedDict, extra_items=ReadOnly[int | str]):
|
|
|
|
title: str
|
|
|
|
|
|
|
|
class Book(BookBase, extra_items=str): # OK
|
|
|
|
year: int # OK
|
|
|
|
|
|
|
|
An important side effect of the inheritance rules is that we can define a
|
|
|
|
TypedDict type that disallows additional items::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieClosed(TypedDict, extra_items=Never):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Here, passing the value :class:`~typing.Never` to ``extra_items`` specifies that
|
2024-02-08 21:55:10 -05:00
|
|
|
there can be no other keys in ``MovieFinal`` other than the known ones.
|
2024-10-18 22:14:30 -04:00
|
|
|
Because of its potential common use, there is a preferred alternative::
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieClosed(TypedDict, closed=True):
|
2024-02-17 17:31:57 -05:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
where we implicitly assume that ``extra_items=Never``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Assignability
|
|
|
|
-------------
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Let ``S`` be the set of keys of the explicitly defined items on a TypedDict
|
|
|
|
type. If it specifies ``extra_items=T``, the TypedDict type is considered to
|
|
|
|
have an infinite set of items that all satisfy the following conditions.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- If ``extra_items`` is read-only:
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- The key's value type is :term:`typing:assignable` to ``T``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-09 08:24:13 -05:00
|
|
|
- The key is not in ``S``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- If ``extra_items`` is not read-only:
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- The key is non-required.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- The key's value type is :term:`typing:consistent` with ``T``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-09 08:24:13 -05:00
|
|
|
- The key is not in ``S``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
For type checking purposes, let ``extra_items`` be a non-required pseudo-item
|
|
|
|
when checking for assignability according to rules defined in the
|
|
|
|
:ref:`Read-only Items <typing:readonly>` section, with a new rule added in bold
|
|
|
|
text as follows:
|
|
|
|
|
|
|
|
A TypedDict type ``B`` is :term:`typing:assignable` to a TypedDict type
|
|
|
|
``A`` if ``B`` is :term:`structurally <typing:structural>` assignable to
|
|
|
|
``A``. This is true if and only if all of the following are satisfied:
|
|
|
|
|
|
|
|
* **[If no key with the same name can be found in ``B``, the 'extra_items'
|
|
|
|
argument is considered the value type of the corresponding key.]**
|
|
|
|
|
|
|
|
* For each item in ``A``, ``B`` has the corresponding key, unless the item in
|
|
|
|
``A`` is read-only, not required, and of top value type
|
|
|
|
(``ReadOnly[NotRequired[object]]``).
|
|
|
|
|
|
|
|
* For each item in ``A``, if ``B`` has the corresponding key, the corresponding
|
|
|
|
value type in ``B`` is assignable to the value type in ``A``.
|
|
|
|
|
|
|
|
* For each non-read-only item in ``A``, its value type is assignable to the
|
|
|
|
corresponding value type in ``B``, and the corresponding key is not read-only
|
|
|
|
in ``B``.
|
|
|
|
|
|
|
|
* For each required key in ``A``, the corresponding key is required in ``B``.
|
|
|
|
|
|
|
|
* For each non-required key in ``A``, if the item is not read-only in ``A``,
|
|
|
|
the corresponding key is not required in ``B``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
The following examples illustrate these checks in action.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
``extra_items`` puts various restrictions on additional items for assignability
|
|
|
|
checks::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class Movie(TypedDict, extra_items=int | None):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieDetails(TypedDict, extra_items=int | None):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
year: NotRequired[int]
|
|
|
|
|
|
|
|
details: MovieDetails = {"name": "Kill Bill Vol. 1", "year": 2003}
|
2024-10-18 22:14:30 -04:00
|
|
|
movie: Movie = details # Not OK. While 'int' is assignable to 'int | None',
|
|
|
|
# 'int | None' is not assignable to 'int'
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieWithYear(TypedDict, extra_items=int | None):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
year: int | None
|
|
|
|
|
|
|
|
details: MovieWithYear = {"name": "Kill Bill Vol. 1", "year": 2003}
|
|
|
|
movie: Movie = details # Not OK. 'year' is not required in 'Movie',
|
|
|
|
# so it shouldn't be required in 'MovieWithYear' either
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Because ``'year'`` is absent in ``Movie``, ``extra_items`` is considered the
|
|
|
|
corresponding key. ``'year'`` being required violates this rule:
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
* For each required key in ``A``, the corresponding key is required in ``B``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
When ``extra_items`` is specified to be read-only on a TypedDict type, it is
|
|
|
|
possible for an item to have a :term:`narrower <typing:narrow>` type than the
|
|
|
|
``extra_items`` argument::
|
|
|
|
|
|
|
|
class Movie(TypedDict, extra_items=ReadOnly[str | int]):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieDetails(TypedDict, extra_items=int):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
year: NotRequired[int]
|
|
|
|
|
|
|
|
details: MovieDetails = {"name": "Kill Bill Vol. 2", "year": 2004}
|
2024-10-18 22:14:30 -04:00
|
|
|
movie: Movie = details # OK. 'int' is assignable to 'str | int'.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
This behaves the same way as if ``year: ReadOnly[str | int]`` is an item
|
|
|
|
explicitly defined in ``Movie``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
``extra_items`` as a pseudo-item follows the same rules that other items have,
|
|
|
|
so when both TypedDicts types specify ``extra_items``, this check is naturally
|
|
|
|
enforced::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieExtraInt(TypedDict, extra_items=int):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieExtraStr(TypedDict, extra_items=str):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
|
|
|
extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
|
|
|
|
extra_str: MovieExtraStr = {"name": "No Country for Old Men", "description": ""}
|
2024-10-18 22:14:30 -04:00
|
|
|
extra_int = extra_str # Not OK. 'str' is not assignable to extra items type 'int'
|
|
|
|
extra_str = extra_int # Not OK. 'int' is not assignable to extra items type 'str'
|
2024-02-17 17:31:57 -05:00
|
|
|
|
|
|
|
A non-closed TypedDict type implicitly allows non-required extra keys of value
|
2024-10-18 22:14:30 -04:00
|
|
|
type ``ReadOnly[object]``. Applying the assignability rules between this type
|
|
|
|
and a closed TypedDict type is allowed::
|
2024-02-17 17:31:57 -05:00
|
|
|
|
|
|
|
class MovieNotClosed(TypedDict):
|
|
|
|
name: str
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-17 17:31:57 -05:00
|
|
|
extra_int: MovieExtraInt = {"name": "No Country for Old Men", "year": 2007}
|
|
|
|
not_closed: MovieNotClosed = {"name": "No Country for Old Men"}
|
2024-10-18 22:14:30 -04:00
|
|
|
extra_int = not_closed # Not OK.
|
|
|
|
# 'extra_items=ReadOnly[object]' implicitly on 'MovieNotClosed'
|
|
|
|
# is not assignable to with 'extra_items=int'
|
2024-02-17 17:31:57 -05:00
|
|
|
not_closed = extra_int # OK
|
|
|
|
|
2024-03-14 11:06:14 -04:00
|
|
|
Interaction with Constructors
|
|
|
|
-----------------------------
|
|
|
|
|
|
|
|
TypedDicts that allow extra items of type ``T`` also allow arbitrary keyword
|
|
|
|
arguments of this type when constructed by calling the class object::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class NonClosedMovie(TypedDict):
|
2024-03-14 11:06:14 -04:00
|
|
|
name: str
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
NonClosedMovie(name="No Country for Old Men") # OK
|
|
|
|
NonClosedMovie(name="No Country for Old Men", year=2007) # Not OK. Unrecognized item
|
2024-03-14 11:06:14 -04:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class ExtraMovie(TypedDict, extra_items=int):
|
2024-03-14 11:06:14 -04:00
|
|
|
name: str
|
|
|
|
|
|
|
|
ExtraMovie(name="No Country for Old Men") # OK
|
|
|
|
ExtraMovie(name="No Country for Old Men", year=2007) # OK
|
|
|
|
ExtraMovie(
|
|
|
|
name="No Country for Old Men",
|
|
|
|
language="English",
|
2024-10-18 22:14:30 -04:00
|
|
|
) # Not OK. Wrong type for extra item 'language'
|
2024-03-14 11:06:14 -04:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
# This implies 'extra_items=Never',
|
|
|
|
# so extra keyword arguments would produce an error
|
2024-03-14 11:06:14 -04:00
|
|
|
class ClosedMovie(TypedDict, closed=True):
|
|
|
|
name: str
|
|
|
|
|
|
|
|
ClosedMovie(name="No Country for Old Men") # OK
|
|
|
|
ClosedMovie(
|
|
|
|
name="No Country for Old Men",
|
|
|
|
year=2007,
|
|
|
|
) # Not OK. Extra items not allowed
|
|
|
|
|
2024-02-08 21:55:10 -05:00
|
|
|
Interaction with Mapping[KT, VT]
|
|
|
|
--------------------------------
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
A TypedDict type can be assignable to ``Mapping[KT, VT]`` types other than
|
|
|
|
``Mapping[str, object]`` as long as all value types of the items on the
|
|
|
|
TypedDict type is :term:`typing:assignable` to ``VT``. This is an extension of this
|
|
|
|
assignability rule from the `typing spec
|
|
|
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#assignability>`__:
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
* A TypedDict with all ``int`` values is not :term:`typing:assignable` to
|
|
|
|
``Mapping[str, int]``, since there may be additional non-``int`` values
|
|
|
|
not visible through the type, due to :term:`typing:structural`
|
|
|
|
assignability. These can be accessed using the ``values()`` and
|
|
|
|
``items()`` methods in ``Mapping``,
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
For example::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class MovieExtraStr(TypedDict, extra_items=str):
|
2024-02-08 21:55:10 -05:00
|
|
|
name: str
|
|
|
|
|
|
|
|
extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
|
|
|
|
str_mapping: Mapping[str, str] = extra_str # OK
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
|
2024-02-08 21:55:10 -05:00
|
|
|
int_str_mapping: Mapping[str, int | str] = extra_int # OK
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Type checkers should be able to infer the precise return types of ``values()``
|
|
|
|
and ``items()`` on such TypedDict types::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
def fun(movie: MovieExtraStr) -> None:
|
|
|
|
reveal_type(movie.items()) # Revealed type is 'dict_items[str, str]'
|
|
|
|
reveal_type(movie.values()) # Revealed type is 'dict_values[str, str]'
|
|
|
|
|
|
|
|
Interaction with dict[KT, VT]
|
|
|
|
-----------------------------
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Note that because the presence of ``extra_items`` on a closed TypedDict type
|
|
|
|
prohibits additional required keys in its :term:`typing:structural`
|
|
|
|
:term:`typing:subtypes <subtype>`, we can determine if the TypedDict type and
|
|
|
|
its structural subtypes will ever have any required key during static analysis.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
The TypedDict type is :term:`typing:assignable` to ``dict[str, VT]`` if all
|
|
|
|
items on the TypedDict type satisfy the following conditions:
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
- The value type of the item is :term:`typing:consistent` with ``VT``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-03-14 11:06:14 -04:00
|
|
|
- The item is not read-only.
|
|
|
|
|
|
|
|
- The item is not required.
|
|
|
|
|
2024-02-08 21:55:10 -05:00
|
|
|
For example::
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class IntDict(TypedDict, extra_items=int):
|
|
|
|
pass
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
class IntDictWithNum(IntDict):
|
|
|
|
num: NotRequired[int]
|
|
|
|
|
|
|
|
def f(x: IntDict) -> None:
|
|
|
|
v: dict[str, int] = x # OK
|
|
|
|
v.clear() # OK
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
not_required_num_dict: IntDictWithNum = {"num": 1, "bar": 2}
|
|
|
|
regular_dict: dict[str, int] = not_required_num_dict # OK
|
|
|
|
f(not_required_num_dict) # OK
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-09 08:24:13 -05:00
|
|
|
In this case, methods that are previously unavailable on a TypedDict are allowed::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
not_required_num.clear() # OK
|
|
|
|
|
|
|
|
reveal_type(not_required_num.popitem()) # OK. Revealed type is tuple[str, int]
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
However, ``dict[str, VT]`` is not necessarily assignable to a TypedDict type,
|
2024-03-14 11:06:14 -04:00
|
|
|
because such dict can be a subtype of dict::
|
|
|
|
|
|
|
|
class CustomDict(dict[str, int]):
|
2024-10-18 22:14:30 -04:00
|
|
|
pass
|
2024-03-14 11:06:14 -04:00
|
|
|
|
|
|
|
not_a_regular_dict: CustomDict = {"num": 1}
|
|
|
|
int_dict: IntDict = not_a_regular_dict # Not OK
|
|
|
|
|
2024-03-14 16:22:26 -04:00
|
|
|
How to Teach This
|
2024-02-17 17:31:57 -05:00
|
|
|
=================
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
The choice of the spelling ``"extra_items"`` is intended to make this
|
2024-03-16 09:29:41 -04:00
|
|
|
feature more understandable to new users compared to shorter alternatives like
|
2024-10-18 22:14:30 -04:00
|
|
|
``"extra"``.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-02-17 17:31:57 -05:00
|
|
|
Details of this should be documented in both the typing spec and the
|
|
|
|
:mod:`typing` documentation.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Backwards Compatibility
|
|
|
|
=======================
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Because ``extra_items`` is an opt-in feature, no existing codebase will break
|
|
|
|
due to this change.
|
2024-02-17 17:31:57 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Note that ``closed`` and ``extra_items`` as keyword arguments do not collide
|
|
|
|
with othere keys when using something like
|
|
|
|
``TD = TypedDict("TD", foo=str, bar=int)``, because this syntax has already
|
|
|
|
been removed in Python 3.13.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Because this is a type-checking feature, it can be made available to older
|
|
|
|
versions as long as the type checker supports it.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Open Issues
|
|
|
|
===========
|
|
|
|
|
|
|
|
Use a Special ``__extra_items__`` Key with the ``closed`` Class Parameter
|
|
|
|
-------------------------------------------------------------------------
|
|
|
|
|
|
|
|
In an earlier revision of this proposal, we discussed an approach that would
|
|
|
|
utilize ``__extra_items__``'s value type to specify the type of extra items
|
|
|
|
accepted, like so::
|
|
|
|
|
|
|
|
class IntDict(TypedDict, closed=True):
|
|
|
|
__extra_items__: int
|
|
|
|
|
|
|
|
where ``closed=True`` is required for ``__extra_items__`` to be treated
|
|
|
|
specially, to avoid key collision.
|
|
|
|
|
|
|
|
Some members of the community concern about the elegance of the syntax.
|
|
|
|
Practiaclly, the key collision with a regular key can be mitigated with
|
|
|
|
workarounds, but since using a reserved key is central to this proposal,
|
|
|
|
there are limited ways forward to address the concerns.
|
|
|
|
|
|
|
|
Support a New Syntax of Specifying Keys
|
|
|
|
---------------------------------------
|
|
|
|
|
|
|
|
By introducing a new syntax that allows specifying string keys, we could
|
|
|
|
deprecate the functional syntax of defining TypedDict types and address the
|
|
|
|
key conflict issues if we decide to reserve a special key to type extra items.
|
|
|
|
|
|
|
|
For example::
|
|
|
|
|
|
|
|
class Foo(TypedDict):
|
|
|
|
name: str # Regular item
|
|
|
|
_: bool # Type of extra items
|
|
|
|
__items__ = {
|
|
|
|
"_": int, # Literal "_" as a key
|
|
|
|
"class": str, # Keyword as a key
|
|
|
|
"tricky.name?": float, # Arbitrary str key
|
|
|
|
}
|
|
|
|
|
|
|
|
This was proposed `here by Jukka
|
|
|
|
<https://discuss.python.org/t/pep-728-typeddict-with-typed-extra-items/45443/115>`__.
|
|
|
|
The ``'_'`` key is chosen for not needing to invent a new name, and its
|
|
|
|
similarity with the match statement.
|
|
|
|
|
|
|
|
This will allow us to deprecate the functional syntax of defining TypedDict
|
|
|
|
types altogether, but there are some disadvantages. `For example
|
|
|
|
<https://github.com/python/peps/pull/4066#discussion_r1806986861>`__:
|
|
|
|
|
|
|
|
- It's less apparent to a reader that ``_: bool`` makes the TypedDict
|
|
|
|
special, relative to adding a class argument like ``extra_items=bool``.
|
|
|
|
|
|
|
|
- It's backwards incompatible with existing TypedDicts using the
|
|
|
|
``_: bool`` key. While such users have a way to get around the issue,
|
|
|
|
it's still a problem for them if they upgrade Python (or
|
|
|
|
typing-extensions).
|
|
|
|
|
|
|
|
- The types don't appear in an annotation context, so their evaluation will
|
|
|
|
not be deferred.
|
|
|
|
|
2024-02-08 21:55:10 -05:00
|
|
|
Rejected Ideas
|
|
|
|
==============
|
|
|
|
|
|
|
|
Allowing Extra Items without Specifying the Type
|
|
|
|
------------------------------------------------
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
``extra=True`` was originally proposed for defining a TypedDict that accepts
|
|
|
|
extra items regardless of the type, like how ``total=True`` works::
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
class ExtraDict(TypedDict, extra=True):
|
2024-02-08 21:55:10 -05:00
|
|
|
pass
|
|
|
|
|
|
|
|
Because it did not offer a way to specify the type of the extra items, the type
|
|
|
|
checkers will need to assume that the type of the extra items is ``Any``, which
|
|
|
|
compromises type safety. Furthermore, the current behavior of TypedDict already
|
2024-10-18 22:14:30 -04:00
|
|
|
allows untyped extra items to be present in runtime, due to
|
|
|
|
:term:`typing:structural` :term:`assignability <typing:assignable>`.
|
|
|
|
``closed=True`` plays a similar role in the current proposal.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Support Extra Items with Intersection
|
|
|
|
-------------------------------------
|
|
|
|
|
|
|
|
Supporting intersections in Python's type system requires a lot of careful
|
2024-02-17 17:31:57 -05:00
|
|
|
consideration, and it can take a long time for the community to reach a
|
2024-02-08 21:55:10 -05:00
|
|
|
consensus on a reasonable design.
|
|
|
|
|
|
|
|
Ideally, extra items in TypedDict should not be blocked by work on
|
|
|
|
intersections, nor does it necessarily need to be supported through
|
|
|
|
intersections.
|
|
|
|
|
|
|
|
Moreover, the intersection between ``Mapping[...]`` and ``TypedDict`` is not
|
2024-10-18 22:14:30 -04:00
|
|
|
equivalent to a TypedDict type with the proposed ``extra_items`` special
|
2024-02-17 17:31:57 -05:00
|
|
|
item, as the value type of all known items in ``TypedDict`` needs to satisfy the
|
2024-02-08 21:55:10 -05:00
|
|
|
is-subtype-of relation with the value type of ``Mapping[...]``.
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
Requiring Type Compatibility of the Known Items with ``extra_items``
|
2024-02-17 17:31:57 -05:00
|
|
|
------------------------------------------------------------------------
|
2024-02-08 21:55:10 -05:00
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
``extra_items`` restricts the value type for keys that are *unknown* to the
|
2024-02-08 21:55:10 -05:00
|
|
|
TypedDict type. So the value type of any *known* item is not necessarily
|
2024-10-18 22:14:30 -04:00
|
|
|
assignable to ``extra_items``, and ``extra_items`` is
|
|
|
|
not necessarily assignable to the value types of all known items.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
This differs from TypeScript's `Index Signatures
|
|
|
|
<https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures>`__
|
|
|
|
syntax, which requires all properties' types to match the string index's type.
|
|
|
|
For example:
|
|
|
|
|
|
|
|
.. code-block:: typescript
|
|
|
|
|
|
|
|
interface MovieWithExtraNumber {
|
|
|
|
name: string // Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
|
|
|
|
[index: string]: number
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MovieWithExtraNumberOrString {
|
|
|
|
name: string // OK
|
|
|
|
[index: string]: number | string
|
|
|
|
}
|
|
|
|
|
2024-02-09 00:31:58 -05:00
|
|
|
This is a known limitation discussed in `TypeScript's issue tracker
|
2024-02-08 21:55:10 -05:00
|
|
|
<https://github.com/microsoft/TypeScript/issues/17867>`__,
|
|
|
|
where it is suggested that there should be a way to exclude the defined keys
|
2024-02-17 17:31:57 -05:00
|
|
|
from the index signature so that it is possible to define a type like
|
2024-02-08 21:55:10 -05:00
|
|
|
``MovieWithExtraNumber``.
|
|
|
|
|
|
|
|
Reference Implementation
|
|
|
|
========================
|
|
|
|
|
2024-10-18 22:14:30 -04:00
|
|
|
An earlier revision of proposal is supported in `pyright 1.1.352
|
2024-03-14 16:22:26 -04:00
|
|
|
<https://github.com/microsoft/pyright/releases/tag/1.1.352>`_, and `pyanalyze
|
|
|
|
0.12.0 <https://github.com/quora/pyanalyze/releases/tag/v0.12.0>`_.
|
2024-02-08 21:55:10 -05:00
|
|
|
|
|
|
|
Acknowledgments
|
|
|
|
===============
|
|
|
|
|
|
|
|
Thanks to Jelle Zijlstra for sponsoring this PEP and providing review feedback,
|
|
|
|
Eric Traut who `proposed the original design
|
2024-02-09 08:24:13 -05:00
|
|
|
<https://mail.python.org/archives/list/typing-sig@python.org/message/3Z72OQWVTOVS6UYUUCCII2UZN56PV5II/>`__
|
2024-02-08 21:55:10 -05:00
|
|
|
this PEP iterates on, and Alice Purcell for offering their perspective as the
|
|
|
|
author of :pep:`705`.
|
|
|
|
|
|
|
|
Copyright
|
|
|
|
=========
|
|
|
|
|
|
|
|
This document is placed in the public domain or under the
|
|
|
|
CC0-1.0-Universal license, whichever is more permissive.
|