PEP 728: Improve specification (#4166)
- Specify that extra_items and closed are also supported with the functional syntax. - Rewrite the rules for `closed=True` and inheritance. I attempted to make `closed=True` exactly equivalent to `extra_items=Never` in terms of inheritance. The semantics as specified in the previous version of the PEP felt harder to understand and less consistent. - Fix some incorrect comments regarding expected type checker errors. - Clarify section on assignability with Mapping - Add section on runtime behavior. I tried to make the intended runtime behavior simple to implement and understand. This makes the runtime simpler but may make life more complicated for tools consuming the metadata. - Fix typos and trailing whitespace
This commit is contained in:
parent
d98e3fa418
commit
484c43d0a7
|
@ -234,38 +234,46 @@ 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
|
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``.
|
must be assignable to the value of ``extra_items`` defined on ``MovieBase``.
|
||||||
|
|
||||||
|
``extra_items`` is also supported with the functional syntax::
|
||||||
|
|
||||||
|
Movie = TypedDict("Movie", {"name": str}, extra_items=int | None)
|
||||||
|
|
||||||
The ``closed`` Class Parameter
|
The ``closed`` Class Parameter
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
When ``closed=True`` is set, no extra items are allowed. This is a shorthand for
|
When ``closed=True`` is set, no extra items are allowed. This is equivalent to
|
||||||
``extra_items=Never``, because there can't be a value type that is assignable to
|
``extra_items=Never``, because there can't be a value type that is assignable to
|
||||||
:class:`~typing.Never`.
|
:class:`~typing.Never`. It is a runtime error to use the ``closed`` and
|
||||||
|
``extra_items`` parameters in the same TypedDict definition.
|
||||||
|
|
||||||
Similar to ``total``, only a literal ``True`` or ``False`` is supported as the
|
Similar to ``total``, only a literal ``True`` or ``False`` is supported as the
|
||||||
value of the ``closed`` argument; ``closed`` is ``False`` by default, which
|
value of the ``closed`` argument. Type checkers should reject any non-literal value.
|
||||||
preserves the previous TypedDict behavior.
|
|
||||||
|
|
||||||
The value of ``closed`` is not inherited through subclassing, but the
|
Passing ``closed=False`` explicitly requests the default TypedDict behavior,
|
||||||
implicitly set ``extra_items=Never`` is. It should be an error to use the
|
where arbitrary other keys may be present and subclasses may add arbitrary items.
|
||||||
default ``closed=False`` when subclassing a closed TypedDict type::
|
It is a type checker error to pass ``closed=False`` if a superclass has
|
||||||
|
``closed=True`` or sets ``extra_items``.
|
||||||
|
|
||||||
|
If ``closed`` is not provided, the behavior is inherited from the superclass.
|
||||||
|
If the superclass is TypedDict itself or the superclass does not have ``closed=True``
|
||||||
|
or the ``extra_items`` parameter, the previous TypedDict behavior is preserved:
|
||||||
|
arbitrary extra items are allowed. If the superclass has ``closed=True``, the
|
||||||
|
child class is also closed.
|
||||||
|
|
||||||
class BaseMovie(TypedDict, closed=True):
|
class BaseMovie(TypedDict, closed=True):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class MovieA(BaseMovie): # Not OK. An explicit 'closed=True' is required
|
class MovieA(BaseMovie): # OK, still closed
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class MovieB(BaseMovie, closed=True): # OK
|
class MovieB(BaseMovie, closed=True): # OK, but redundant
|
||||||
pass
|
pass
|
||||||
|
|
||||||
Setting both ``closed`` and ``extra_items`` when defining a TypedDict type
|
class MovieC(BaseMovie, closed=False): # Type checker error
|
||||||
should always be a runtime error::
|
pass
|
||||||
|
|
||||||
class Person(TypedDict, closed=False, extra_items=bool): # Not OK. 'closed' and 'extra_items' are incompatible
|
As a consequence of ``closed=True`` being equivalent to ``extra_items=Never``,
|
||||||
name: str
|
the same rules that apply to ``extra_items=Never`` also apply to
|
||||||
|
|
||||||
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
|
``closed=True``. It is possible to use ``closed=True`` when subclassing if the
|
||||||
``extra_items`` argument is a read-only type::
|
``extra_items`` argument is a read-only type::
|
||||||
|
|
||||||
|
@ -275,7 +283,7 @@ The same rules that apply to ``extra_items=Never`` should also apply to
|
||||||
class MovieClosed(Movie, closed=True): # OK
|
class MovieClosed(Movie, closed=True): # OK
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class MovieNever(Movie, extra_items=Never): # Not OK. 'closed=True' is preferred
|
class MovieNever(Movie, extra_items=Never): # OK, but 'closed=True' is preferred
|
||||||
pass
|
pass
|
||||||
|
|
||||||
This will be further discussed in
|
This will be further discussed in
|
||||||
|
@ -286,6 +294,10 @@ is assumed to allow non-required extra items of value type ``ReadOnly[object]``
|
||||||
during inheritance or assignability checks. This preserves the existing behavior
|
during inheritance or assignability checks. This preserves the existing behavior
|
||||||
of TypedDict.
|
of TypedDict.
|
||||||
|
|
||||||
|
``closed`` is also supported with the functional syntax::
|
||||||
|
|
||||||
|
Movie = TypedDict("Movie", {"name": str}, closed=True)
|
||||||
|
|
||||||
Interaction with Totality
|
Interaction with Totality
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
@ -378,20 +390,17 @@ added in a subclass, all of the following conditions should apply:
|
||||||
|
|
||||||
- The item's value type is :term:`typing:consistent` with ``T``
|
- The item's value type is :term:`typing:consistent` with ``T``
|
||||||
|
|
||||||
- If ``extra_items`` is not overriden, the subclass inherits it as-is.
|
- If ``extra_items`` is not overridden, the subclass inherits it as-is.
|
||||||
|
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
class MovieBase(TypedDict, extra_items=int | None):
|
class MovieBase(TypedDict, extra_items=int | None):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class AdaptedMovie(MovieBase): # Not OK. 'bool' is not assignable to 'int | None'
|
class MovieRequiredYear(MovieBase): # Not OK. Required key 'year' is not known to 'MovieBase'
|
||||||
adapted_from_novel: bool
|
|
||||||
|
|
||||||
class MovieRequiredYear(MovieBase): # Not OK. Required key 'year' is not known to 'Parent'
|
|
||||||
year: int | None
|
year: int | None
|
||||||
|
|
||||||
class MovieNotRequiredYear(MovieBase): # Not OK. 'int | None' is not assignable to 'int'
|
class MovieNotRequiredYear(MovieBase): # Not OK. 'int | None' is not consistent with 'int'
|
||||||
year: NotRequired[int]
|
year: NotRequired[int]
|
||||||
|
|
||||||
class MovieWithYear(MovieBase): # OK
|
class MovieWithYear(MovieBase): # OK
|
||||||
|
@ -578,17 +587,13 @@ arguments of this type when constructed by calling the class object::
|
||||||
Interaction with Mapping[KT, VT]
|
Interaction with Mapping[KT, VT]
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
A TypedDict type can be assignable to ``Mapping[KT, VT]`` types other than
|
A TypedDict type is :term:`typing:assignable` to a type of the form ``Mapping[str, VT]``
|
||||||
``Mapping[str, object]`` as long as all value types of the items on the
|
when all value types of the items in the TypedDict
|
||||||
TypedDict type is :term:`typing:assignable` to ``VT``. This is an extension of this
|
are assignable to ``VT``. For the purpose of this rule, a
|
||||||
|
TypedDict that does not have ``extra_items=`` or ``closed=`` set is considered
|
||||||
|
to have an item with a value of type ``object``. This extends the current
|
||||||
assignability rule from the `typing spec
|
assignability rule from the `typing spec
|
||||||
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#assignability>`__:
|
<https://typing.readthedocs.io/en/latest/spec/typeddict.html#assignability>`__.
|
||||||
|
|
||||||
* 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``,
|
|
||||||
|
|
||||||
For example::
|
For example::
|
||||||
|
|
||||||
|
@ -598,6 +603,10 @@ For example::
|
||||||
extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
|
extra_str: MovieExtraStr = {"name": "Blade Runner", "summary": ""}
|
||||||
str_mapping: Mapping[str, str] = extra_str # OK
|
str_mapping: Mapping[str, str] = extra_str # OK
|
||||||
|
|
||||||
|
class MovieExtraInt(TypedDict, extra_items=int):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
extra_int: MovieExtraInt = {"name": "Blade Runner", "year": 1982}
|
||||||
int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
|
int_mapping: Mapping[str, int] = extra_int # Not OK. 'int | str' is not assignable with 'int'
|
||||||
int_str_mapping: Mapping[str, int | str] = extra_int # OK
|
int_str_mapping: Mapping[str, int | str] = extra_int # OK
|
||||||
|
|
||||||
|
@ -611,7 +620,7 @@ and ``items()`` on such TypedDict types::
|
||||||
Interaction with dict[KT, VT]
|
Interaction with dict[KT, VT]
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
Note that because the presence of ``extra_items`` on a closed TypedDict type
|
Because the presence of ``extra_items`` on a closed TypedDict type
|
||||||
prohibits additional required keys in its :term:`typing:structural`
|
prohibits additional required keys in its :term:`typing:structural`
|
||||||
:term:`typing:subtypes <subtype>`, we can determine if the TypedDict type and
|
:term:`typing:subtypes <subtype>`, we can determine if the TypedDict type and
|
||||||
its structural subtypes will ever have any required key during static analysis.
|
its structural subtypes will ever have any required key during static analysis.
|
||||||
|
@ -656,6 +665,24 @@ because such dict can be a subtype of dict::
|
||||||
not_a_regular_dict: CustomDict = {"num": 1}
|
not_a_regular_dict: CustomDict = {"num": 1}
|
||||||
int_dict: IntDict = not_a_regular_dict # Not OK
|
int_dict: IntDict = not_a_regular_dict # Not OK
|
||||||
|
|
||||||
|
Runtime behavior
|
||||||
|
----------------
|
||||||
|
|
||||||
|
At runtime, it is an error to pass both the ``closed`` and ``extra_items``
|
||||||
|
arguments in the same TypedDict definition, whether using the class syntax or
|
||||||
|
the functional syntax. For simplicity, the runtime does not check other invalid
|
||||||
|
combinations involving inheritance.
|
||||||
|
|
||||||
|
For introspection, the ``closed`` and ``extra_items`` arguments are mapped to
|
||||||
|
two new attributes on the resulting TypedDict object: ``__closed__`` and
|
||||||
|
``__extra_items__``. These attributes reflect exactly what was passed to the
|
||||||
|
TypedDict constructor, without considering superclasses.
|
||||||
|
|
||||||
|
If ``closed`` is not passed, the value of ``__closed__`` is None. If ``extra_items``
|
||||||
|
is not passed, the value of ``__extra_items__`` is the new sentinel object
|
||||||
|
``typing.NoExtraItems``. (It cannot be ``None``, because ``extra_items=None`` is a
|
||||||
|
valid definition that indicates all extra items must be ``None``.)
|
||||||
|
|
||||||
How to Teach This
|
How to Teach This
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
@ -673,7 +700,7 @@ Because ``extra_items`` is an opt-in feature, no existing codebase will break
|
||||||
due to this change.
|
due to this change.
|
||||||
|
|
||||||
Note that ``closed`` and ``extra_items`` as keyword arguments do not collide
|
Note that ``closed`` and ``extra_items`` as keyword arguments do not collide
|
||||||
with othere keys when using something like
|
with other keys when using something like
|
||||||
``TD = TypedDict("TD", foo=str, bar=int)``, because this syntax has already
|
``TD = TypedDict("TD", foo=str, bar=int)``, because this syntax has already
|
||||||
been removed in Python 3.13.
|
been removed in Python 3.13.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue