Skip to content

Commit f2132fc

Browse files
JelleZijlstracarljmpicnixz
authored
gh-117516: Implement typing.TypeIs (#117517)
See PEP 742. Co-authored-by: Carl Meyer <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 5718324 commit f2132fc

File tree

5 files changed

+236
-39
lines changed

5 files changed

+236
-39
lines changed

Doc/library/typing.rst

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,22 +1385,23 @@ These can be used as types in annotations. They all support subscription using
13851385
.. versionadded:: 3.9
13861386

13871387

1388-
.. data:: TypeGuard
1388+
.. data:: TypeIs
13891389

1390-
Special typing construct for marking user-defined type guard functions.
1390+
Special typing construct for marking user-defined type predicate functions.
13911391

1392-
``TypeGuard`` can be used to annotate the return type of a user-defined
1393-
type guard function. ``TypeGuard`` only accepts a single type argument.
1394-
At runtime, functions marked this way should return a boolean.
1392+
``TypeIs`` can be used to annotate the return type of a user-defined
1393+
type predicate function. ``TypeIs`` only accepts a single type argument.
1394+
At runtime, functions marked this way should return a boolean and take at
1395+
least one positional argument.
13951396

1396-
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
1397+
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
13971398
type checkers to determine a more precise type of an expression within a
13981399
program's code flow. Usually type narrowing is done by analyzing
13991400
conditional code flow and applying the narrowing to a block of code. The
1400-
conditional expression here is sometimes referred to as a "type guard"::
1401+
conditional expression here is sometimes referred to as a "type predicate"::
14011402

14021403
def is_str(val: str | float):
1403-
# "isinstance" type guard
1404+
# "isinstance" type predicate
14041405
if isinstance(val, str):
14051406
# Type of ``val`` is narrowed to ``str``
14061407
...
@@ -1409,8 +1410,73 @@ These can be used as types in annotations. They all support subscription using
14091410
...
14101411

14111412
Sometimes it would be convenient to use a user-defined boolean function
1412-
as a type guard. Such a function should use ``TypeGuard[...]`` as its
1413-
return type to alert static type checkers to this intention.
1413+
as a type predicate. Such a function should use ``TypeIs[...]`` or
1414+
:data:`TypeGuard` as its return type to alert static type checkers to
1415+
this intention. ``TypeIs`` usually has more intuitive behavior than
1416+
``TypeGuard``, but it cannot be used when the input and output types
1417+
are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the
1418+
function does not return ``True`` for all instances of the narrowed type.
1419+
1420+
Using ``-> TypeIs[NarrowedType]`` tells the static type checker that for a given
1421+
function:
1422+
1423+
1. The return value is a boolean.
1424+
2. If the return value is ``True``, the type of its argument
1425+
is the intersection of the argument's original type and ``NarrowedType``.
1426+
3. If the return value is ``False``, the type of its argument
1427+
is narrowed to exclude ``NarrowedType``.
1428+
1429+
For example::
1430+
1431+
from typing import assert_type, final, TypeIs
1432+
1433+
class Parent: pass
1434+
class Child(Parent): pass
1435+
@final
1436+
class Unrelated: pass
1437+
1438+
def is_parent(val: object) -> TypeIs[Parent]:
1439+
return isinstance(val, Parent)
1440+
1441+
def run(arg: Child | Unrelated):
1442+
if is_parent(arg):
1443+
# Type of ``arg`` is narrowed to the intersection
1444+
# of ``Parent`` and ``Child``, which is equivalent to
1445+
# ``Child``.
1446+
assert_type(arg, Child)
1447+
else:
1448+
# Type of ``arg`` is narrowed to exclude ``Parent``,
1449+
# so only ``Unrelated`` is left.
1450+
assert_type(arg, Unrelated)
1451+
1452+
The type inside ``TypeIs`` must be consistent with the type of the
1453+
function's argument; if it is not, static type checkers will raise
1454+
an error. An incorrectly written ``TypeIs`` function can lead to
1455+
unsound behavior in the type system; it is the user's responsibility
1456+
to write such functions in a type-safe manner.
1457+
1458+
If a ``TypeIs`` function is a class or instance method, then the type in
1459+
``TypeIs`` maps to the type of the second parameter after ``cls`` or
1460+
``self``.
1461+
1462+
In short, the form ``def foo(arg: TypeA) -> TypeIs[TypeB]: ...``,
1463+
means that if ``foo(arg)`` returns ``True``, then ``arg`` is an instance
1464+
of ``TypeB``, and if it returns ``False``, it is not an instance of ``TypeB``.
1465+
1466+
``TypeIs`` also works with type variables. For more information, see
1467+
:pep:`742` (Narrowing types with ``TypeIs``).
1468+
1469+
.. versionadded:: 3.13
1470+
1471+
1472+
.. data:: TypeGuard
1473+
1474+
Special typing construct for marking user-defined type predicate functions.
1475+
1476+
Type predicate functions are user-defined functions that return whether their
1477+
argument is an instance of a particular type.
1478+
``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different
1479+
effects on type checking behavior (see below).
14141480

14151481
Using ``-> TypeGuard`` tells the static type checker that for a given
14161482
function:
@@ -1419,6 +1485,8 @@ These can be used as types in annotations. They all support subscription using
14191485
2. If the return value is ``True``, the type of its argument
14201486
is the type inside ``TypeGuard``.
14211487

1488+
``TypeGuard`` also works with type variables. See :pep:`647` for more details.
1489+
14221490
For example::
14231491

14241492
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
14331501
# Type of ``val`` remains as ``list[object]``.
14341502
print("Not a list of strings!")
14351503

1436-
If ``is_str_list`` is a class or instance method, then the type in
1437-
``TypeGuard`` maps to the type of the second parameter after ``cls`` or
1438-
``self``.
1439-
1440-
In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``,
1441-
means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from
1442-
``TypeA`` to ``TypeB``.
1443-
1444-
.. note::
1445-
1446-
``TypeB`` need not be a narrower form of ``TypeA`` -- it can even be a
1447-
wider form. The main reason is to allow for things like
1448-
narrowing ``list[object]`` to ``list[str]`` even though the latter
1449-
is not a subtype of the former, since ``list`` is invariant.
1450-
The responsibility of writing type-safe type guards is left to the user.
1451-
1452-
``TypeGuard`` also works with type variables. See :pep:`647` for more details.
1504+
``TypeIs`` and ``TypeGuard`` differ in the following ways:
1505+
1506+
* ``TypeIs`` requires the narrowed type to be a subtype of the input type, while
1507+
``TypeGuard`` does not. The main reason is to allow for things like
1508+
narrowing ``list[object]`` to ``list[str]`` even though the latter
1509+
is not a subtype of the former, since ``list`` is invariant.
1510+
* When a ``TypeGuard`` function returns ``True``, type checkers narrow the type of the
1511+
variable to exactly the ``TypeGuard`` type. When a ``TypeIs`` function returns ``True``,
1512+
type checkers can infer a more precise type combining the previously known type of the
1513+
variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.)
1514+
* When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of
1515+
the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow
1516+
the type of the variable to exclude the ``TypeIs`` type.
14531517

14541518
.. versionadded:: 3.10
14551519

Doc/whatsnew/3.13.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ Interpreter improvements:
8787
Performance improvements are modest -- we expect to be improving this
8888
over the next few releases.
8989

90+
New typing features:
91+
92+
* :pep:`742`: :data:`typing.TypeIs` was added, providing more intuitive
93+
type narrowing behavior.
9094

9195
New Features
9296
============

Lib/test/test_typing.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from typing import Self, LiteralString
3939
from typing import TypeAlias
4040
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
41-
from typing import TypeGuard
41+
from typing import TypeGuard, TypeIs
4242
import abc
4343
import textwrap
4444
import typing
@@ -5207,6 +5207,7 @@ def test_subclass_special_form(self):
52075207
Literal[1, 2],
52085208
Concatenate[int, ParamSpec("P")],
52095209
TypeGuard[int],
5210+
TypeIs[range],
52105211
):
52115212
with self.subTest(msg=obj):
52125213
with self.assertRaisesRegex(
@@ -6748,6 +6749,7 @@ class C(Generic[T]): pass
67486749
self.assertEqual(get_args(NotRequired[int]), (int,))
67496750
self.assertEqual(get_args(TypeAlias), ())
67506751
self.assertEqual(get_args(TypeGuard[int]), (int,))
6752+
self.assertEqual(get_args(TypeIs[range]), (range,))
67516753
Ts = TypeVarTuple('Ts')
67526754
self.assertEqual(get_args(Ts), ())
67536755
self.assertEqual(get_args((*Ts,)[0]), (Ts,))
@@ -9592,6 +9594,56 @@ def test_no_isinstance(self):
95929594
issubclass(int, TypeGuard)
95939595

95949596

9597+
class TypeIsTests(BaseTestCase):
9598+
def test_basics(self):
9599+
TypeIs[int] # OK
9600+
9601+
def foo(arg) -> TypeIs[int]: ...
9602+
self.assertEqual(gth(foo), {'return': TypeIs[int]})
9603+
9604+
with self.assertRaises(TypeError):
9605+
TypeIs[int, str]
9606+
9607+
def test_repr(self):
9608+
self.assertEqual(repr(TypeIs), 'typing.TypeIs')
9609+
cv = TypeIs[int]
9610+
self.assertEqual(repr(cv), 'typing.TypeIs[int]')
9611+
cv = TypeIs[Employee]
9612+
self.assertEqual(repr(cv), 'typing.TypeIs[%s.Employee]' % __name__)
9613+
cv = TypeIs[tuple[int]]
9614+
self.assertEqual(repr(cv), 'typing.TypeIs[tuple[int]]')
9615+
9616+
def test_cannot_subclass(self):
9617+
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
9618+
class C(type(TypeIs)):
9619+
pass
9620+
with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE):
9621+
class D(type(TypeIs[int])):
9622+
pass
9623+
with self.assertRaisesRegex(TypeError,
9624+
r'Cannot subclass typing\.TypeIs'):
9625+
class E(TypeIs):
9626+
pass
9627+
with self.assertRaisesRegex(TypeError,
9628+
r'Cannot subclass typing\.TypeIs\[int\]'):
9629+
class F(TypeIs[int]):
9630+
pass
9631+
9632+
def test_cannot_init(self):
9633+
with self.assertRaises(TypeError):
9634+
TypeIs()
9635+
with self.assertRaises(TypeError):
9636+
type(TypeIs)()
9637+
with self.assertRaises(TypeError):
9638+
type(TypeIs[Optional[int]])()
9639+
9640+
def test_no_isinstance(self):
9641+
with self.assertRaises(TypeError):
9642+
isinstance(1, TypeIs[int])
9643+
with self.assertRaises(TypeError):
9644+
issubclass(int, TypeIs)
9645+
9646+
95959647
SpecialAttrsP = typing.ParamSpec('SpecialAttrsP')
95969648
SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex)
95979649

@@ -9691,6 +9743,7 @@ def test_special_attrs(self):
96919743
typing.Optional: 'Optional',
96929744
typing.TypeAlias: 'TypeAlias',
96939745
typing.TypeGuard: 'TypeGuard',
9746+
typing.TypeIs: 'TypeIs',
96949747
typing.TypeVar: 'TypeVar',
96959748
typing.Union: 'Union',
96969749
typing.Self: 'Self',
@@ -9705,6 +9758,7 @@ def test_special_attrs(self):
97059758
typing.Literal[True, 2]: 'Literal',
97069759
typing.Optional[Any]: 'Optional',
97079760
typing.TypeGuard[Any]: 'TypeGuard',
9761+
typing.TypeIs[Any]: 'TypeIs',
97089762
typing.Union[Any]: 'Any',
97099763
typing.Union[int, float]: 'Union',
97109764
# Incompatible special forms (tested in test_special_attrs2)

0 commit comments

Comments
 (0)