Skip to content

Commit

Permalink
PEP 747: Require TypeExpr(X | Y) syntax to spell value of type TypeEx…
Browse files Browse the repository at this point in the history
…pr[X | Y] (#3893)
  • Loading branch information
davidfstr authored Aug 13, 2024
1 parent 1696887 commit a6fb1e8
Showing 1 changed file with 36 additions and 80 deletions.
116 changes: 36 additions & 80 deletions peps/pep-0747.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

- Yes:

- As a value expression, ``x | y`` has type equal to the return type of ``type(x).__or__``
if ``type(x)`` overrides the ``__or__`` method.
::

- 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]``.
if isassignable(value, TypeExpr(int | str)): ...

- As a value expression, ``x | y`` has type equal to the return type of ``type(y).__ror__``
if ``type(y)`` overrides the ``__ror__`` method.
- No:

- 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]``.
::

- As a value expression, ``x | y`` has type ``UnionType``
in all other situations.
if isassignable(value, int | str): ...

- This rule is intended to be consistent with the preexisting fallback rule
used by static type checkers.
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"``
Expand Down Expand Up @@ -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
''''''''''''''''''''''''

Expand Down Expand Up @@ -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
''''''''''''''''''''

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 :

Expand All @@ -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
Expand Down Expand Up @@ -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
=========

Expand Down

0 comments on commit a6fb1e8

Please sign in to comment.