PEP 747: Require TypeExpr(X | Y) syntax to spell value of type TypeExpr[X | Y] (#3893)

This commit is contained in:
David Foster 2024-08-13 15:17:36 -04:00 committed by GitHub
parent 1696887355
commit a6fb1e8d5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 36 additions and 80 deletions

View File

@ -442,27 +442,23 @@ so must be disambiguated based on its argument type:
**Union**: The type expression ``T1 | T2`` is ambiguous with
the value ``int1 | int2``, ``set1 | set2``, ``dict1 | dict2``, and more,
so must be disambiguated based on its argument types:
so must use the explicit ``TypeExpr(...)`` syntax:
- As a value expression, ``x | y`` has type equal to the return type of ``type(x).__or__``
if ``type(x)`` overrides the ``__or__`` method.
- Yes:
- When ``x`` has type ``builtins.type``, ``types.GenericAlias``, or the
internal type of a typing special form, ``type(x).__or__`` has a return type
in the format ``TypeExpr[T1 | T2]``.
::
- As a value expression, ``x | y`` has type equal to the return type of ``type(y).__ror__``
if ``type(y)`` overrides the ``__ror__`` method.
if isassignable(value, TypeExpr(int | str)): ...
- When ``y`` has type ``builtins.type``, ``types.GenericAlias``, or the
internal type of a typing special form, ``type(y).__ror__`` has a return type
in the format ``TypeExpr[T1 | T2]``.
- No:
- As a value expression, ``x | y`` has type ``UnionType``
in all other situations.
::
- This rule is intended to be consistent with the preexisting fallback rule
used by static type checkers.
if isassignable(value, int | str): ...
Future PEPs may make it possible to recognize the value expression ``T1 | T2`` directly as an
implicit TypeExpr value and avoid the need to use the explicit ``TypeExpr(...)`` syntax,
but that work is :ref:`deferred for now <recognize_uniontype_as_implicit_typeexpr_value>`.
The **stringified type expression** ``"T"`` is ambiguous with both
the stringified annotation expression ``"T"``
@ -551,16 +547,6 @@ but not the other way around:
- ``type[Any]`` is assignable to ``TypeExpr[Any]``. (But not the
other way around.)
Relationship with UnionType
'''''''''''''''''''''''''''
``TypeExpr[U]`` is a subtype of ``UnionType`` iff ``U`` is
the type expression ``X | Y | ...``:
- ``TypeExpr[X | Y | ...]`` is a subtype of ``UnionType``.
``UnionType`` is assignable to ``TypeExpr[Any]``.
Relationship with object
''''''''''''''''''''''''
@ -610,29 +596,6 @@ The following signatures related to type expressions introduce
- ``typing.cast``
- ``typing.assert_type``
The following signatures transforming union type expressions introduce
``TypeExpr`` where previously ``UnionType`` existed so that a more-precise
``TypeExpr`` type can be inferred:
- ``builtins.type[T].__or__``
- Old: ``def __or__(self, value: Any, /) -> types.UnionType: ...``
- New: ``def __or__[T2](self, value: TypeExpr[T2], /) -> TypeExpr[T | T2]: ...``
- ``builtins.type[T].__ror__``
- Old: ``def __ror__(self, value: Any, /) -> types.UnionType: ...``
- New: ``def __ror__[T1](self, value: TypeExpr[T1], /) -> TypeExpr[T1 | T]: ...``
- ``types.GenericAlias.{__or__,__ror__}``
- «the internal type of a typing special form»``.{__or__,__ror__}``
However the implementations of those methods continue to return ``UnionType``
instances at runtime so that runtime ``isinstance`` checks like
``isinstance('42', int | str)`` and ``isinstance(int | str, UnionType)``
continue to work.
Unchanged signatures
''''''''''''''''''''
@ -666,31 +629,11 @@ not propose those changes now:
- Returns annotation expressions
The following signatures accepting union type expressions continue
to use ``UnionType``:
- ``builtins.isinstance``
- ``builtins.issubclass``
- ``typing.get_origin`` (used in an ``@overload``)
The following signatures transforming union type expressions continue
to use ``UnionType`` because it is not possible to infer a more-precise
``TypeExpr`` type:
- ``types.UnionType.{__or__,__ror__}``
Backwards Compatibility
=======================
As a value expression, ``X | Y`` previously had type ``UnionType`` (via :pep:`604`)
but this PEP gives it the more-precise static type ``TypeExpr[X | Y]``
(a subtype of ``UnionType``) while continuing to return a ``UnionType`` instance at runtime.
Preserving compability with ``UnionType`` is important because ``UnionType``
supports ``isinstance`` checks, unlike ``TypeExpr``, and existing code relies
on being able to perform those checks.
The rules for recognizing other kinds of type expression objects
The rules for recognizing type expression objects
in a value expression context were not previously defined, so static type checkers
`varied in what types were assigned <https://discuss.python.org/t/typeform-spelling-for-a-type-annotation-object-at-runtime/51435/34>`_
to such objects. Existing programs manipulating type expression objects
@ -741,10 +684,16 @@ spell simple **class objects** like ``int``, ``str``, ``list``, or ``MyClass``.
including those with brackets (like ``list[int]``) or pipes (like ``int | None``),
and including special types like ``Any``, ``LiteralString``, or ``Never``.
A ``TypeExpr`` variable looks similar to a ``TypeAlias`` definition, but
can only be used where a dynamic value is expected.
``TypeAlias`` (and the ``type`` statement) by contrast define a name that can
be used where a fixed type is expected:
A ``TypeExpr`` variable (``maybe_float: TypeExpr``) looks similar to
a ``TypeAlias`` definition (``MaybeFloat: TypeAlias``), but ``TypeExpr``
can only be used where a dynamic value is expected:
- No:
::
maybe_float: TypeExpr = float | None
def sqrt(n: float) -> maybe_float: ... # ERROR: Can't use TypeExpr value in a type annotation
- Okay, but discouraged in Python 3.12+:
@ -760,13 +709,6 @@ be used where a fixed type is expected:
type MaybeFloat = float | None
def sqrt(n: float) -> MaybeFloat: ...
- No:
::
maybe_float: TypeExpr = float | None
def sqrt(n: float) -> maybe_float: ... # ERROR: Can't use TypeExpr value in a type annotation
It is uncommon for a programmer to define their *own* function which accepts
a ``TypeExpr`` parameter or returns a ``TypeExpr`` value. Instead it is more common
for a programmer to pass a literal type expression to an *existing* function
@ -1072,6 +1014,20 @@ The example above could be more-straightforwardly written as the equivalent:
def checkcast(typx: TypeExpr[T], value: object) -> T:
.. _recognize_uniontype_as_implicit_typeexpr_value:
Recognize (T1 | T2) as an implicit TypeExpr value
-------------------------------------------------
It would be nice if a value expression like ``int | str`` could be recognized
as an implicit ``TypeExpr`` value and be used directly in a context where a
``TypeExpr`` was expected. However making that possible would require making
changes to the rules that type checkers use for the ``|`` operator. These rules
are currently underspecified and would need to be make explicit first,
before making changes to them. The PEP author is not sufficently motivated to
take on that specification work at the time of writing.
Footnotes
=========