mirror of https://github.com/python/cpython
gh-117516: Implement typing.TypeIs (#117517)
See PEP 742. Co-authored-by: Carl Meyer <carl@oddbird.net> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
parent
57183241af
commit
f2132fcd2a
|
@ -1385,22 +1385,23 @@ These can be used as types in annotations. They all support subscription using
|
|||
.. versionadded:: 3.9
|
||||
|
||||
|
||||
.. data:: TypeGuard
|
||||
.. data:: TypeIs
|
||||
|
||||
Special typing construct for marking user-defined type guard functions.
|
||||
Special typing construct for marking user-defined type predicate functions.
|
||||
|
||||
``TypeGuard`` can be used to annotate the return type of a user-defined
|
||||
type guard function. ``TypeGuard`` only accepts a single type argument.
|
||||
At runtime, functions marked this way should return a boolean.
|
||||
``TypeIs`` can be used to annotate the return type of a user-defined
|
||||
type predicate function. ``TypeIs`` only accepts a single type argument.
|
||||
At runtime, functions marked this way should return a boolean and take at
|
||||
least one positional argument.
|
||||
|
||||
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
|
||||
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
|
||||
type checkers to determine a more precise type of an expression within a
|
||||
program's code flow. Usually type narrowing is done by analyzing
|
||||
conditional code flow and applying the narrowing to a block of code. The
|
||||
conditional expression here is sometimes referred to as a "type guard"::
|
||||
conditional expression here is sometimes referred to as a "type predicate"::
|
||||
|
||||
def is_str(val: str | float):
|
||||
# "isinstance" type guard
|
||||
# "isinstance" type predicate
|
||||
if isinstance(val, str):
|
||||
# Type of ``val`` is narrowed to ``str``
|
||||
...
|
||||
|
@ -1409,8 +1410,73 @@ These can be used as types in annotations. They all support subscription using
|
|||
...
|
||||
|
||||
Sometimes it would be convenient to use a user-defined boolean function
|
||||
as a type guard. Such a function should use ``TypeGuard[...]`` as its
|
||||
return type to alert static type checkers to this intention.
|
||||
as a type predicate. Such a function should use ``TypeIs[...]`` or
|
||||
:data:`TypeGuard` as its return type to alert static type checkers to
|
||||
this intention. ``TypeIs`` usually has more intuitive behavior than
|
||||
``TypeGuard``, but it cannot be used when the input and output types
|
||||
are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the
|
||||
function does not return ``True`` for all instances of the narrowed type.
|
||||
|
||||
Using ``-> TypeIs[NarrowedType]`` tells the static type checker that for a given
|
||||
function:
|
||||
|
||||
1. The return value is a boolean.
|
||||
2. If the return value is ``True``, the type of its argument
|
||||
is the intersection of the argument's original type and ``NarrowedType``.
|
||||
3. If the return value is ``False``, the type of its argument
|
||||
is narrowed to exclude ``NarrowedType``.
|
||||
|
||||
For example::
|
||||
|
||||
from typing import assert_type, final, TypeIs
|
||||
|
||||
class Parent: pass
|
||||
class Child(Parent): pass
|
||||
@final
|
||||
class Unrelated: pass
|
||||
|
||||
def is_parent(val: object) -> TypeIs[Parent]:
|
||||
return isinstance(val, Parent)
|
||||
|
||||
def run(arg: Child | Unrelated):
|
||||
if is_parent(arg):
|
||||
# Type of ``arg`` is narrowed to the intersection
|
||||
# of ``Parent`` and ``Child``, which is equivalent to
|
||||
# ``Child``.
|
||||
assert_type(arg, Child)
|
||||
else:
|
||||
# Type of ``arg`` is narrowed to exclude ``Parent``,
|
||||
# so only ``Unrelated`` is left.
|
||||
assert_type(arg, Unrelated)
|
||||
|
||||
The type inside ``TypeIs`` must be consistent with the type of the
|
||||
function's argument; if it is not, static type checkers will raise
|
||||
an error. An incorrectly written ``TypeIs`` function can lead to
|
||||
unsound behavior in the type system; it is the user's responsibility
|
||||
to write such functions in a type-safe manner.
|
||||
|
||||
If a ``TypeIs`` function is a class or instance method, then the type in
|
||||
``TypeIs`` maps to the type of the second parameter after ``cls`` or
|
||||
``self``.
|
||||
|
||||
In short, the form ``def foo(arg: TypeA) -> TypeIs[TypeB]: ...``,
|
||||
means that if ``foo(arg)`` returns ``True``, then ``arg`` is an instance
|
||||
of ``TypeB``, and if it returns ``False``, it is not an instance of ``TypeB``.
|
||||
|
||||
``TypeIs`` also works with type variables. For more information, see
|
||||
:pep:`742` (Narrowing types with ``TypeIs``).
|
||||
|
||||
.. versionadded:: 3.13
|
||||
|
||||
|
||||
.. data:: TypeGuard
|
||||
|
||||
Special typing construct for marking user-defined type predicate functions.
|
||||
|
||||
Type predicate functions are user-defined functions that return whether their
|
||||
argument is an instance of a particular type.
|
||||
``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different
|
||||
effects on type checking behavior (see below).
|
||||
|
||||
Using ``-> TypeGuard`` tells the static type checker that for a given
|
||||
function:
|
||||
|
@ -1419,6 +1485,8 @@ These can be used as types in annotations. They all support subscription using
|
|||
2. If the return value is ``True``, the type of its argument
|
||||
is the type inside ``TypeGuard``.
|
||||
|
||||
``TypeGuard`` also works with type variables. See :pep:`647` for more details.
|
||||
|
||||
For example::
|
||||
|
||||
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
|
||||
|
@ -1433,23 +1501,19 @@ These can be used as types in annotations. They all support subscription using
|
|||
# Type of ``val`` remains as ``list[object]``.
|
||||
print("Not a list of strings!")
|
||||
|
||||
If ``is_str_list`` is a class or instance method, then the type in
|
||||
``TypeGuard`` maps to the type of the second parameter after ``cls`` or
|
||||
``self``.
|
||||
``TypeIs`` and ``TypeGuard`` differ in the following ways:
|
||||
|
||||
In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``,
|
||||
means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from
|
||||
``TypeA`` to ``TypeB``.
|
||||
|
||||
.. note::
|
||||
|
||||
``TypeB`` need not be a narrower form of ``TypeA`` -- it can even be a
|
||||
wider form. The main reason is to allow for things like
|
||||
narrowing ``list[object]`` to ``list[str]`` even though the latter
|
||||
is not a subtype of the former, since ``list`` is invariant.
|
||||
The responsibility of writing type-safe type guards is left to the user.
|
||||
|
||||
``TypeGuard`` also works with type variables. See :pep:`647` for more details.
|
||||
* ``TypeIs`` requires the narrowed type to be a subtype of the input type, while
|
||||
``TypeGuard`` does not. The main reason is to allow for things like
|
||||
narrowing ``list[object]`` to ``list[str]`` even though the latter
|
||||
is not a subtype of the former, since ``list`` is invariant.
|
||||
* When a ``TypeGuard`` function returns ``True``, type checkers narrow the type of the
|
||||
variable to exactly the ``TypeGuard`` type. When a ``TypeIs`` function returns ``True``,
|
||||
type checkers can infer a more precise type combining the previously known type of the
|
||||
variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.)
|
||||
* When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of
|
||||
the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow
|
||||
the type of the variable to exclude the ``TypeIs`` type.
|
||||
|
||||
.. versionadded:: 3.10
|
||||
|
||||
|
|
|
@ -87,6 +87,10 @@ Interpreter improvements:
|
|||
Performance improvements are modest -- we expect to be improving this
|
||||
over the next few releases.
|
||||
|
||||
New typing features:
|
||||
|
||||
* :pep:`742`: :data:`typing.TypeIs` was added, providing more intuitive
|
||||
type narrowing behavior.
|
||||
|
||||
New Features
|
||||
============
|
||||
|
|
|
@ -38,7 +38,7 @@ from typing import Annotated, ForwardRef
|
|||
from typing import Self, LiteralString
|
||||
from typing import TypeAlias
|
||||
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
|
||||
from typing import TypeGuard
|
||||
from typing import TypeGuard, TypeIs
|
||||
import abc
|
||||
import textwrap
|
||||
import typing
|
||||
|
@ -5207,6 +5207,7 @@ class GenericTests(BaseTestCase):
|
|||
Literal[1, 2],
|
||||
Concatenate[int, ParamSpec("P")],
|
||||
TypeGuard[int],
|
||||
TypeIs[range],
|
||||
):
|
||||
with self.subTest(msg=obj):
|
||||
with self.assertRaisesRegex(
|
||||
|
@ -6748,6 +6749,7 @@ class GetUtilitiesTestCase(TestCase):
|
|||
self.assertEqual(get_args(NotRequired[int]), (int,))
|
||||
self.assertEqual(get_args(TypeAlias), ())
|
||||
self.assertEqual(get_args(TypeGuard[int]), (int,))
|
||||
self.assertEqual(get_args(TypeIs[range]), (range,))
|
||||
Ts = TypeVarTuple('Ts')
|
||||
self.assertEqual(get_args(Ts), ())
|
||||
self.assertEqual(get_args((*Ts,)[0]), (Ts,))
|
||||
|
@ -9592,6 +9594,56 @@ class TypeGuardTests(BaseTestCase):
|
|||
issubclass(int, TypeGuard)
|
||||
|
||||
|
||||
class TypeIsTests(BaseTestCase):
|
||||
def test_basics(self):
|
||||
TypeIs[int] # OK
|
||||
|
||||
def foo(arg) -> TypeIs[int]: ...
|
||||
self.assertEqual(gth(foo), {'return': TypeIs[int]})
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
TypeIs[int, str]
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(repr(TypeIs), 'typing.TypeIs')
|
||||
cv = TypeIs[int]
|
||||
self.assertEqual(repr(cv), 'typing.TypeIs[int]')
|
||||
cv = TypeIs[Employee]
|
||||
self.assertEqual(repr(cv), 'typing.TypeIs[%s.Employee]' % __name__)
|
||||
cv = TypeIs[tuple[int]]
|
||||
self.assertEqual(repr(cv), 'typing.TypeIs[tuple[int]]')
|
||||
|
||||
def test_cannot_subclass(self):
|
||||
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
|
||||
class C(type(TypeIs)):
|
||||
pass
|
||||
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
|
||||
class D(type(TypeIs[int])):
|
||||
pass
|
||||
with self.assertRaisesRegex(TypeError,
|
||||
r'Cannot subclass typing\.TypeIs'):
|
||||
class E(TypeIs):
|
||||
pass
|
||||
with self.assertRaisesRegex(TypeError,
|
||||
r'Cannot subclass typing\.TypeIs\[int\]'):
|
||||
class F(TypeIs[int]):
|
||||
pass
|
||||
|
||||
def test_cannot_init(self):
|
||||
with self.assertRaises(TypeError):
|
||||
TypeIs()
|
||||
with self.assertRaises(TypeError):
|
||||
type(TypeIs)()
|
||||
with self.assertRaises(TypeError):
|
||||
type(TypeIs[Optional[int]])()
|
||||
|
||||
def test_no_isinstance(self):
|
||||
with self.assertRaises(TypeError):
|
||||
isinstance(1, TypeIs[int])
|
||||
with self.assertRaises(TypeError):
|
||||
issubclass(int, TypeIs)
|
||||
|
||||
|
||||
SpecialAttrsP = typing.ParamSpec('SpecialAttrsP')
|
||||
SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex)
|
||||
|
||||
|
@ -9691,6 +9743,7 @@ class SpecialAttrsTests(BaseTestCase):
|
|||
typing.Optional: 'Optional',
|
||||
typing.TypeAlias: 'TypeAlias',
|
||||
typing.TypeGuard: 'TypeGuard',
|
||||
typing.TypeIs: 'TypeIs',
|
||||
typing.TypeVar: 'TypeVar',
|
||||
typing.Union: 'Union',
|
||||
typing.Self: 'Self',
|
||||
|
@ -9705,6 +9758,7 @@ class SpecialAttrsTests(BaseTestCase):
|
|||
typing.Literal[True, 2]: 'Literal',
|
||||
typing.Optional[Any]: 'Optional',
|
||||
typing.TypeGuard[Any]: 'TypeGuard',
|
||||
typing.TypeIs[Any]: 'TypeIs',
|
||||
typing.Union[Any]: 'Any',
|
||||
typing.Union[int, float]: 'Union',
|
||||
# Incompatible special forms (tested in test_special_attrs2)
|
||||
|
|
|
@ -153,6 +153,7 @@ __all__ = [
|
|||
'TYPE_CHECKING',
|
||||
'TypeAlias',
|
||||
'TypeGuard',
|
||||
'TypeIs',
|
||||
'TypeAliasType',
|
||||
'Unpack',
|
||||
]
|
||||
|
@ -818,28 +819,31 @@ def Concatenate(self, parameters):
|
|||
|
||||
@_SpecialForm
|
||||
def TypeGuard(self, parameters):
|
||||
"""Special typing construct for marking user-defined type guard functions.
|
||||
"""Special typing construct for marking user-defined type predicate functions.
|
||||
|
||||
``TypeGuard`` can be used to annotate the return type of a user-defined
|
||||
type guard function. ``TypeGuard`` only accepts a single type argument.
|
||||
type predicate function. ``TypeGuard`` only accepts a single type argument.
|
||||
At runtime, functions marked this way should return a boolean.
|
||||
|
||||
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
|
||||
type checkers to determine a more precise type of an expression within a
|
||||
program's code flow. Usually type narrowing is done by analyzing
|
||||
conditional code flow and applying the narrowing to a block of code. The
|
||||
conditional expression here is sometimes referred to as a "type guard".
|
||||
conditional expression here is sometimes referred to as a "type predicate".
|
||||
|
||||
Sometimes it would be convenient to use a user-defined boolean function
|
||||
as a type guard. Such a function should use ``TypeGuard[...]`` as its
|
||||
return type to alert static type checkers to this intention.
|
||||
as a type predicate. Such a function should use ``TypeGuard[...]`` or
|
||||
``TypeIs[...]`` as its return type to alert static type checkers to
|
||||
this intention. ``TypeGuard`` should be used over ``TypeIs`` when narrowing
|
||||
from an incompatible type (e.g., ``list[object]`` to ``list[int]``) or when
|
||||
the function does not return ``True`` for all instances of the narrowed type.
|
||||
|
||||
Using ``-> TypeGuard`` tells the static type checker that for a given
|
||||
function:
|
||||
Using ``-> TypeGuard[NarrowedType]`` tells the static type checker that
|
||||
for a given function:
|
||||
|
||||
1. The return value is a boolean.
|
||||
2. If the return value is ``True``, the type of its argument
|
||||
is the type inside ``TypeGuard``.
|
||||
is ``NarrowedType``.
|
||||
|
||||
For example::
|
||||
|
||||
|
@ -860,7 +864,7 @@ def TypeGuard(self, parameters):
|
|||
type-unsafe results. The main reason is to allow for things like
|
||||
narrowing ``list[object]`` to ``list[str]`` even though the latter is not
|
||||
a subtype of the former, since ``list`` is invariant. The responsibility of
|
||||
writing type-safe type guards is left to the user.
|
||||
writing type-safe type predicates is left to the user.
|
||||
|
||||
``TypeGuard`` also works with type variables. For more information, see
|
||||
PEP 647 (User-Defined Type Guards).
|
||||
|
@ -869,6 +873,75 @@ def TypeGuard(self, parameters):
|
|||
return _GenericAlias(self, (item,))
|
||||
|
||||
|
||||
@_SpecialForm
|
||||
def TypeIs(self, parameters):
|
||||
"""Special typing construct for marking user-defined type predicate functions.
|
||||
|
||||
``TypeIs`` can be used to annotate the return type of a user-defined
|
||||
type predicate function. ``TypeIs`` only accepts a single type argument.
|
||||
At runtime, functions marked this way should return a boolean and accept
|
||||
at least one argument.
|
||||
|
||||
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
|
||||
type checkers to determine a more precise type of an expression within a
|
||||
program's code flow. Usually type narrowing is done by analyzing
|
||||
conditional code flow and applying the narrowing to a block of code. The
|
||||
conditional expression here is sometimes referred to as a "type predicate".
|
||||
|
||||
Sometimes it would be convenient to use a user-defined boolean function
|
||||
as a type predicate. Such a function should use ``TypeIs[...]`` or
|
||||
``TypeGuard[...]`` as its return type to alert static type checkers to
|
||||
this intention. ``TypeIs`` usually has more intuitive behavior than
|
||||
``TypeGuard``, but it cannot be used when the input and output types
|
||||
are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the
|
||||
function does not return ``True`` for all instances of the narrowed type.
|
||||
|
||||
Using ``-> TypeIs[NarrowedType]`` tells the static type checker that for
|
||||
a given function:
|
||||
|
||||
1. The return value is a boolean.
|
||||
2. If the return value is ``True``, the type of its argument
|
||||
is the intersection of the argument's original type and
|
||||
``NarrowedType``.
|
||||
3. If the return value is ``False``, the type of its argument
|
||||
is narrowed to exclude ``NarrowedType``.
|
||||
|
||||
For example::
|
||||
|
||||
from typing import assert_type, final, TypeIs
|
||||
|
||||
class Parent: pass
|
||||
class Child(Parent): pass
|
||||
@final
|
||||
class Unrelated: pass
|
||||
|
||||
def is_parent(val: object) -> TypeIs[Parent]:
|
||||
return isinstance(val, Parent)
|
||||
|
||||
def run(arg: Child | Unrelated):
|
||||
if is_parent(arg):
|
||||
# Type of ``arg`` is narrowed to the intersection
|
||||
# of ``Parent`` and ``Child``, which is equivalent to
|
||||
# ``Child``.
|
||||
assert_type(arg, Child)
|
||||
else:
|
||||
# Type of ``arg`` is narrowed to exclude ``Parent``,
|
||||
# so only ``Unrelated`` is left.
|
||||
assert_type(arg, Unrelated)
|
||||
|
||||
The type inside ``TypeIs`` must be consistent with the type of the
|
||||
function's argument; if it is not, static type checkers will raise
|
||||
an error. An incorrectly written ``TypeIs`` function can lead to
|
||||
unsound behavior in the type system; it is the user's responsibility
|
||||
to write such functions in a type-safe manner.
|
||||
|
||||
``TypeIs`` also works with type variables. For more information, see
|
||||
PEP 742 (Narrowing types with ``TypeIs``).
|
||||
"""
|
||||
item = _type_check(parameters, f'{self} accepts only single type.')
|
||||
return _GenericAlias(self, (item,))
|
||||
|
||||
|
||||
class ForwardRef(_Final, _root=True):
|
||||
"""Internal wrapper to hold a forward reference."""
|
||||
|
||||
|
@ -1241,11 +1314,12 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
|
|||
# A = Callable[[], None] # _CallableGenericAlias
|
||||
# B = Callable[[T], None] # _CallableGenericAlias
|
||||
# C = B[int] # _CallableGenericAlias
|
||||
# * Parameterized `Final`, `ClassVar` and `TypeGuard`:
|
||||
# * Parameterized `Final`, `ClassVar`, `TypeGuard`, and `TypeIs`:
|
||||
# # All _GenericAlias
|
||||
# Final[int]
|
||||
# ClassVar[float]
|
||||
# TypeVar[bool]
|
||||
# TypeGuard[bool]
|
||||
# TypeIs[range]
|
||||
|
||||
def __init__(self, origin, args, *, inst=True, name=None):
|
||||
super().__init__(origin, inst=inst, name=name)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Add :data:`typing.TypeIs`, implementing :pep:`742`. Patch by Jelle Zijlstra.
|
Loading…
Reference in New Issue