PEP 742: Add Discussions-To; use TypeIs (#3665)

This commit is contained in:
Jelle Zijlstra 2024-02-15 17:16:03 -08:00 committed by GitHub
parent e1f09321ac
commit b9cc1fcc61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 41 additions and 42 deletions

View File

@ -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
=============== ===============