PEP 747: Require TypeExpr(X | Y) syntax to spell value of type TypeExpr[X | Y] (#3893)
This commit is contained in:
parent
1696887355
commit
a6fb1e8d5a
|
@ -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
|
||||
=========
|
||||
|
||||
|
|
Loading…
Reference in New Issue