PEP 742: Add Discussions-To; use TypeIs (#3665)
This commit is contained in:
parent
e1f09321ac
commit
b9cc1fcc61
|
@ -1,20 +1,22 @@
|
||||||
PEP: 742
|
PEP: 742
|
||||||
Title: Narrowing types with TypeNarrower
|
Title: Narrowing types with TypeIs
|
||||||
Author: Jelle Zijlstra <jelle.zijlstra@gmail.com>
|
Author: Jelle Zijlstra <jelle.zijlstra@gmail.com>
|
||||||
|
Discussions-To: https://discuss.python.org/t/pep-742-narrowing-types-with-typenarrower/45613
|
||||||
Status: Draft
|
Status: Draft
|
||||||
Type: Standards Track
|
Type: Standards Track
|
||||||
Topic: Typing
|
Topic: Typing
|
||||||
Created: 07-Feb-2024
|
Created: 07-Feb-2024
|
||||||
Python-Version: 3.13
|
Python-Version: 3.13
|
||||||
|
Post-History: `11-Feb-2024 <https://discuss.python.org/t/pep-742-narrowing-types-with-typenarrower/45613>`__
|
||||||
Replaces: 724
|
Replaces: 724
|
||||||
|
|
||||||
|
|
||||||
Abstract
|
Abstract
|
||||||
========
|
========
|
||||||
|
|
||||||
This PEP proposes a new special form, ``TypeNarrower``, to allow annotating functions that can be used
|
This PEP proposes a new special form, ``TypeIs``, to allow annotating functions that can be used
|
||||||
to narrow the type of a value, similar to the builtin :py:func:`isinstance`. Unlike the existing
|
to narrow the type of a value, similar to the builtin :py:func:`isinstance`. Unlike the existing
|
||||||
:py:data:`typing.TypeGuard` special form, ``TypeNarrower`` can narrow the type in both the ``if``
|
:py:data:`typing.TypeGuard` special form, ``TypeIs`` can narrow the type in both the ``if``
|
||||||
and ``else`` branches of a conditional.
|
and ``else`` branches of a conditional.
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,23 +101,23 @@ adding a new special form with the desired semantics.
|
||||||
|
|
||||||
We acknowledge that this leads to an unfortunate situation where there are two constructs with
|
We acknowledge that this leads to an unfortunate situation where there are two constructs with
|
||||||
a similar purpose and similar semantics. We believe that users are more likely to want the behavior
|
a similar purpose and similar semantics. We believe that users are more likely to want the behavior
|
||||||
of ``TypeNarrower``, the new form proposed in this PEP, and therefore we recommend that documentation
|
of ``TypeIs``, the new form proposed in this PEP, and therefore we recommend that documentation
|
||||||
emphasize ``TypeNarrower`` over ``TypeGuard`` as a more commonly applicable tool. However, the semantics of
|
emphasize ``TypeIs`` over ``TypeGuard`` as a more commonly applicable tool. However, the semantics of
|
||||||
``TypeGuard`` are occasionally useful, and we do not propose to deprecate or remove it. In the long
|
``TypeGuard`` are occasionally useful, and we do not propose to deprecate or remove it. In the long
|
||||||
run, most users should use ``TypeNarrower``, and ``TypeGuard`` should be reserved for rare cases
|
run, most users should use ``TypeIs``, and ``TypeGuard`` should be reserved for rare cases
|
||||||
where its behavior is specifically desired.
|
where its behavior is specifically desired.
|
||||||
|
|
||||||
|
|
||||||
Specification
|
Specification
|
||||||
=============
|
=============
|
||||||
|
|
||||||
A new special form, ``TypeNarrower``, is added to the :py:mod:`typing`
|
A new special form, ``TypeIs``, is added to the :py:mod:`typing`
|
||||||
module. Its usage, behavior, and runtime implementation are similar to
|
module. Its usage, behavior, and runtime implementation are similar to
|
||||||
those of :py:data:`typing.TypeGuard`.
|
those of :py:data:`typing.TypeGuard`.
|
||||||
|
|
||||||
It accepts a single
|
It accepts a single
|
||||||
argument and can be used as the return type of a function. A function annotated as returning a
|
argument and can be used as the return type of a function. A function annotated as returning a
|
||||||
``TypeNarrower`` is called a type narrowing function. Type narrowing functions must return ``bool``
|
``TypeIs`` is called a type narrowing function. Type narrowing functions must return ``bool``
|
||||||
values, and the type checker should verify that all return paths return
|
values, and the type checker should verify that all return paths return
|
||||||
``bool``.
|
``bool``.
|
||||||
|
|
||||||
|
@ -129,17 +131,17 @@ to the second parameter (after ``self`` or ``cls``).
|
||||||
Type narrowing behavior
|
Type narrowing behavior
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
To specify the behavior of ``TypeNarrower``, we use the following terminology:
|
To specify the behavior of ``TypeIs``, we use the following terminology:
|
||||||
|
|
||||||
* I = ``TypeNarrower`` input type
|
* I = ``TypeIs`` input type
|
||||||
* R = ``TypeNarrower`` return type
|
* R = ``TypeIs`` return type
|
||||||
* A = Type of argument passed to type narrowing function (pre-narrowed)
|
* A = Type of argument passed to type narrowing function (pre-narrowed)
|
||||||
* NP = Narrowed type (positive; used when ``TypeNarrower`` returned ``True``)
|
* NP = Narrowed type (positive; used when ``TypeIs`` returned ``True``)
|
||||||
* NN = Narrowed type (negative; used when ``TypeNarrower`` returned ``False``)
|
* NN = Narrowed type (negative; used when ``TypeIs`` returned ``False``)
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def narrower(x: I) -> TypeNarrower[R]: ...
|
def narrower(x: I) -> TypeIs[R]: ...
|
||||||
|
|
||||||
def func1(val: A):
|
def func1(val: A):
|
||||||
if narrower(val):
|
if narrower(val):
|
||||||
|
@ -165,9 +167,9 @@ Examples
|
||||||
|
|
||||||
Type narrowing is applied in both the positive and negative case::
|
Type narrowing is applied in both the positive and negative case::
|
||||||
|
|
||||||
from typing import TypeNarrower, assert_type
|
from typing import TypeIs, assert_type
|
||||||
|
|
||||||
def is_str(x: object) -> TypeNarrower[str]:
|
def is_str(x: object) -> TypeIs[str]:
|
||||||
return isinstance(x, str)
|
return isinstance(x, str)
|
||||||
|
|
||||||
def f(x: str | int) -> None:
|
def f(x: str | int) -> None:
|
||||||
|
@ -180,10 +182,10 @@ The final narrowed type may be narrower than **R**, due to the constraints of th
|
||||||
argument's previously-known type::
|
argument's previously-known type::
|
||||||
|
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from typing import Any, TypeNarrower, assert_type
|
from typing import Any, TypeIs, assert_type
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
def isawaitable(x: object) -> TypeNarrower[Awaitable[Any]]:
|
def isawaitable(x: object) -> TypeIs[Awaitable[Any]]:
|
||||||
return inspect.isawaitable(x)
|
return inspect.isawaitable(x)
|
||||||
|
|
||||||
def f(x: Awaitable[int] | int) -> None:
|
def f(x: Awaitable[int] | int) -> None:
|
||||||
|
@ -194,35 +196,35 @@ argument's previously-known type::
|
||||||
|
|
||||||
It is an error to narrow to a type that is not consistent with the input type::
|
It is an error to narrow to a type that is not consistent with the input type::
|
||||||
|
|
||||||
from typing import TypeNarrower
|
from typing import TypeIs
|
||||||
|
|
||||||
def is_str(x: int) -> TypeNarrower[str]: # Type checker error
|
def is_str(x: int) -> TypeIs[str]: # Type checker error
|
||||||
...
|
...
|
||||||
|
|
||||||
Subtyping
|
Subtyping
|
||||||
---------
|
---------
|
||||||
|
|
||||||
``TypeNarrower`` is not a subtype of ``bool``.
|
``TypeIs`` is not a subtype of ``bool``.
|
||||||
The type ``Callable[..., TypeNarrower[int]]`` is not assignable to
|
The type ``Callable[..., TypeIs[int]]`` is not assignable to
|
||||||
``Callable[..., bool]`` or ``Callable[..., TypeGuard[int]]``, and vice versa.
|
``Callable[..., bool]`` or ``Callable[..., TypeGuard[int]]``, and vice versa.
|
||||||
This restriction is carried over from :pep:`647`. It may be possible to relax
|
This restriction is carried over from :pep:`647`. It may be possible to relax
|
||||||
it in the future, but that is outside the scope of this PEP.
|
it in the future, but that is outside the scope of this PEP.
|
||||||
|
|
||||||
Unlike ``TypeGuard``, ``TypeNarrower`` is invariant in its argument type:
|
Unlike ``TypeGuard``, ``TypeIs`` is invariant in its argument type:
|
||||||
``TypeNarrower[B]`` is not a subtype of ``TypeNarrower[A]``,
|
``TypeIs[B]`` is not a subtype of ``TypeIs[A]``,
|
||||||
even if ``B`` is a subtype of ``A``.
|
even if ``B`` is a subtype of ``A``.
|
||||||
To see why, consider the following example::
|
To see why, consider the following example::
|
||||||
|
|
||||||
def takes_narrower(x: int | str, narrower: Callable[[object], TypeNarrower[int]]):
|
def takes_narrower(x: int | str, narrower: Callable[[object], TypeIs[int]]):
|
||||||
if narrower(x):
|
if narrower(x):
|
||||||
print(x + 1) # x is an int
|
print(x + 1) # x is an int
|
||||||
else:
|
else:
|
||||||
print("Hello " + x) # x is a str
|
print("Hello " + x) # x is a str
|
||||||
|
|
||||||
def is_bool(x: object) -> TypeNarrower[bool]:
|
def is_bool(x: object) -> TypeIs[bool]:
|
||||||
return isinstance(x, bool)
|
return isinstance(x, bool)
|
||||||
|
|
||||||
takes_narrower(1, is_bool) # Error: is_bool is not a TypeNarrower[int]
|
takes_narrower(1, is_bool) # Error: is_bool is not a TypeIs[int]
|
||||||
|
|
||||||
(Note that ``bool`` is a subtype of ``int``.)
|
(Note that ``bool`` is a subtype of ``int``.)
|
||||||
This code fails at runtime, because the narrower returns ``False`` (1 is not a ``bool``)
|
This code fails at runtime, because the narrower returns ``False`` (1 is not a ``bool``)
|
||||||
|
@ -246,12 +248,12 @@ None known.
|
||||||
How to Teach This
|
How to Teach This
|
||||||
=================
|
=================
|
||||||
|
|
||||||
Introductions to typing should cover ``TypeNarrower`` when discussing how to narrow types,
|
Introductions to typing should cover ``TypeIs`` when discussing how to narrow types,
|
||||||
along with discussion of other narrowing constructs such as :py:func:`isinstance`. The
|
along with discussion of other narrowing constructs such as :py:func:`isinstance`. The
|
||||||
documentation should emphasize ``TypeNarrower`` over :py:data:`typing.TypeGuard`; while the
|
documentation should emphasize ``TypeIs`` over :py:data:`typing.TypeGuard`; while the
|
||||||
latter is not being deprecated and its behavior is occasionally useful, we expect that the
|
latter is not being deprecated and its behavior is occasionally useful, we expect that the
|
||||||
behavior of ``TypeNarrower`` is usually more intuitive, and most users should reach for
|
behavior of ``TypeIs`` is usually more intuitive, and most users should reach for
|
||||||
``TypeNarrower`` first.
|
``TypeIs`` first.
|
||||||
|
|
||||||
|
|
||||||
Reference Implementation
|
Reference Implementation
|
||||||
|
@ -270,7 +272,7 @@ Change the behavior of ``TypeGuard``
|
||||||
|
|
||||||
:pep:`724` previously proposed changing the specified behavior of :py:data:`typing.TypeGuard` so
|
:pep:`724` previously proposed changing the specified behavior of :py:data:`typing.TypeGuard` so
|
||||||
that if the return type of the guard is consistent with the input type, the behavior proposed
|
that if the return type of the guard is consistent with the input type, the behavior proposed
|
||||||
here for ``TypeNarrower`` would apply. This proposal has some important advantages: because it
|
here for ``TypeIs`` would apply. This proposal has some important advantages: because it
|
||||||
does not require any runtime changes, it requires changes only in type checkers, making it easier
|
does not require any runtime changes, it requires changes only in type checkers, making it easier
|
||||||
for users to take advantage of the new, usually more intuitive behavior.
|
for users to take advantage of the new, usually more intuitive behavior.
|
||||||
|
|
||||||
|
@ -296,15 +298,13 @@ If we do not make any change, users will continue to encounter the same unintuit
|
||||||
``TypeGuard``, and the type system will be unable to properly represent common type narrowing functions
|
``TypeGuard``, and the type system will be unable to properly represent common type narrowing functions
|
||||||
like ``inspect.isawaitable``.
|
like ``inspect.isawaitable``.
|
||||||
|
|
||||||
Open Issues
|
Alternative names
|
||||||
===========
|
-----------------
|
||||||
|
|
||||||
Naming
|
This PEP currently proposes the name ``TypeIs``, emphasizing that the special form ``TypeIs[T]``
|
||||||
------
|
returns whether the argument is of type ``T``, and mirroring
|
||||||
|
`TypeScript's syntax <https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates>`__.
|
||||||
This PEP currently proposes the name ``TypeNarrower``, emphasizing that the special form narrows
|
Other names were considered, including in an earlier version of this PEP.
|
||||||
the type of its argument. However, other names have been suggested, and we are open to using a
|
|
||||||
different name.
|
|
||||||
|
|
||||||
Options include:
|
Options include:
|
||||||
|
|
||||||
|
@ -317,8 +317,7 @@ Options include:
|
||||||
version of type narrowing than :py:data:`typing.TypeGuard`.
|
version of type narrowing than :py:data:`typing.TypeGuard`.
|
||||||
* ``TypeCheck`` (`post by Nicolas Tessore <https://discuss.python.org/t/pep-724-stricter-type-guards/34124/59>`__):
|
* ``TypeCheck`` (`post by Nicolas Tessore <https://discuss.python.org/t/pep-724-stricter-type-guards/34124/59>`__):
|
||||||
emphasizes the binary nature of the check.
|
emphasizes the binary nature of the check.
|
||||||
* ``TypeIs``: emphasizes that the function returns whether the argument is of that type; mirrors
|
* ``TypeNarrower``: emphasizes that the function narrows its argument type. Used in an earlier version of this PEP.
|
||||||
`TypeScript's syntax <https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates>`__.
|
|
||||||
|
|
||||||
Acknowledgments
|
Acknowledgments
|
||||||
===============
|
===============
|
||||||
|
|
Loading…
Reference in New Issue