Skip to content

Commit 05ab4b6

Browse files
bpo-43766: Implement PEP 647 (User-Defined Type Guards) in typing.py (#25282)
1 parent d925133 commit 05ab4b6

File tree

5 files changed

+175
-0
lines changed

5 files changed

+175
-0
lines changed

Doc/library/typing.rst

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,80 @@ These can be used as types in annotations using ``[]``, each having a unique syn
933933

934934
.. versionadded:: 3.9
935935

936+
937+
.. data:: TypeGuard
938+
939+
Special typing form used to annotate the return type of a user-defined
940+
type guard function. ``TypeGuard`` only accepts a single type argument.
941+
At runtime, functions marked this way should return a boolean.
942+
943+
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
944+
type checkers to determine a more precise type of an expression within a
945+
program's code flow. Usually type narrowing is done by analyzing
946+
conditional code flow and applying the narrowing to a block of code. The
947+
conditional expression here is sometimes referred to as a "type guard"::
948+
949+
def is_str(val: Union[str, float]):
950+
# "isinstance" type guard
951+
if isinstance(val, str):
952+
# Type of ``val`` is narrowed to ``str``
953+
...
954+
else:
955+
# Else, type of ``val`` is narrowed to ``float``.
956+
...
957+
958+
Sometimes it would be convenient to use a user-defined boolean function
959+
as a type guard. Such a function should use ``TypeGuard[...]`` as its
960+
return type to alert static type checkers to this intention.
961+
962+
Using ``-> TypeGuard`` tells the static type checker that for a given
963+
function:
964+
965+
1. The return value is a boolean.
966+
2. If the return value is ``True``, the type of its argument
967+
is the type inside ``TypeGuard``.
968+
969+
For example::
970+
971+
def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
972+
'''Determines whether all objects in the list are strings'''
973+
return all(isinstance(x, str) for x in val)
974+
975+
def func1(val: List[object]):
976+
if is_str_list(val):
977+
# Type of ``val`` is narrowed to List[str]
978+
print(" ".join(val))
979+
else:
980+
# Type of ``val`` remains as List[object]
981+
print("Not a list of strings!")
982+
983+
If ``is_str_list`` is a class or instance method, then the type in
984+
``TypeGuard`` maps to the type of the second parameter after ``cls`` or
985+
``self``.
986+
987+
In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``,
988+
means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from
989+
``TypeA`` to ``TypeB``.
990+
991+
.. note::
992+
993+
``TypeB`` need not be a narrower form of ``TypeA`` -- it can even be a
994+
wider form. The main reason is to allow for things like
995+
narrowing ``List[object]`` to ``List[str]`` even though the latter
996+
is not a subtype of the former, since ``List`` is invariant.
997+
The responsibility of
998+
writing type-safe type guards is left to the user. Even if
999+
the type guard function passes type checks, it may still fail at runtime.
1000+
The type guard function may perform erroneous checks and return wrong
1001+
booleans. Consequently, the type it promises in ``TypeGuard[TypeB]`` may
1002+
not hold.
1003+
1004+
``TypeGuard`` also works with type variables. For more information, see
1005+
:pep:`647` (User-Defined Type Guards).
1006+
1007+
.. versionadded:: 3.10
1008+
1009+
9361010
Building generic types
9371011
""""""""""""""""""""""
9381012

Doc/whatsnew/3.10.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,16 @@ See :pep:`613` for more details.
743743
744744
(Contributed by Mikhail Golubev in :issue:`41923`.)
745745
746+
PEP 647: User-Defined Type Guards
747+
---------------------------------
748+
749+
:data:`TypeGuard` has been added to the :mod:`typing` module to annotate
750+
type guard functions and improve information provided to static type checkers
751+
during type narrowing. For more information, please see :data:`TypeGuard`\ 's
752+
documentation, and :pep:`647`.
753+
754+
(Contributed by Ken Jin and Guido van Rossum in :issue:`43766`.
755+
PEP written by Eric Traut.)
746756
747757
Other Language Changes
748758
======================

Lib/test/test_typing.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from typing import Annotated, ForwardRef
2727
from typing import TypeAlias
2828
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
29+
from typing import TypeGuard
2930
import abc
3031
import typing
3132
import weakref
@@ -4377,6 +4378,45 @@ def test_valid_uses(self):
43774378
self.assertEqual(C4.__parameters__, (T, P))
43784379

43794380

4381+
class TypeGuardTests(BaseTestCase):
4382+
def test_basics(self):
4383+
TypeGuard[int] # OK
4384+
4385+
def foo(arg) -> TypeGuard[int]: ...
4386+
self.assertEqual(gth(foo), {'return': TypeGuard[int]})
4387+
4388+
def test_repr(self):
4389+
self.assertEqual(repr(TypeGuard), 'typing.TypeGuard')
4390+
cv = TypeGuard[int]
4391+
self.assertEqual(repr(cv), 'typing.TypeGuard[int]')
4392+
cv = TypeGuard[Employee]
4393+
self.assertEqual(repr(cv), 'typing.TypeGuard[%s.Employee]' % __name__)
4394+
cv = TypeGuard[tuple[int]]
4395+
self.assertEqual(repr(cv), 'typing.TypeGuard[tuple[int]]')
4396+
4397+
def test_cannot_subclass(self):
4398+
with self.assertRaises(TypeError):
4399+
class C(type(TypeGuard)):
4400+
pass
4401+
with self.assertRaises(TypeError):
4402+
class C(type(TypeGuard[int])):
4403+
pass
4404+
4405+
def test_cannot_init(self):
4406+
with self.assertRaises(TypeError):
4407+
TypeGuard()
4408+
with self.assertRaises(TypeError):
4409+
type(TypeGuard)()
4410+
with self.assertRaises(TypeError):
4411+
type(TypeGuard[Optional[int]])()
4412+
4413+
def test_no_isinstance(self):
4414+
with self.assertRaises(TypeError):
4415+
isinstance(1, TypeGuard[int])
4416+
with self.assertRaises(TypeError):
4417+
issubclass(int, TypeGuard)
4418+
4419+
43804420
class AllTests(BaseTestCase):
43814421
"""Tests for __all__."""
43824422

Lib/typing.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
'Text',
120120
'TYPE_CHECKING',
121121
'TypeAlias',
122+
'TypeGuard',
122123
]
123124

124125
# The pseudo-submodules 're' and 'io' are part of the public
@@ -567,6 +568,54 @@ def Concatenate(self, parameters):
567568
return _ConcatenateGenericAlias(self, parameters)
568569

569570

571+
@_SpecialForm
572+
def TypeGuard(self, parameters):
573+
"""Special typing form used to annotate the return type of a user-defined
574+
type guard function. ``TypeGuard`` only accepts a single type argument.
575+
At runtime, functions marked this way should return a boolean.
576+
577+
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
578+
type checkers to determine a more precise type of an expression within a
579+
program's code flow. Usually type narrowing is done by analyzing
580+
conditional code flow and applying the narrowing to a block of code. The
581+
conditional expression here is sometimes referred to as a "type guard".
582+
583+
Sometimes it would be convenient to use a user-defined boolean function
584+
as a type guard. Such a function should use ``TypeGuard[...]`` as its
585+
return type to alert static type checkers to this intention.
586+
587+
Using ``-> TypeGuard`` tells the static type checker that for a given
588+
function:
589+
590+
1. The return value is a boolean.
591+
2. If the return value is ``True``, the type of its argument
592+
is the type inside ``TypeGuard``.
593+
594+
For example::
595+
596+
def is_str(val: Union[str, float]):
597+
# "isinstance" type guard
598+
if isinstance(val, str):
599+
# Type of ``val`` is narrowed to ``str``
600+
...
601+
else:
602+
# Else, type of ``val`` is narrowed to ``float``.
603+
...
604+
605+
Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
606+
form of ``TypeA`` (it can even be a wider form) and this may lead to
607+
type-unsafe results. The main reason is to allow for things like
608+
narrowing ``List[object]`` to ``List[str]`` even though the latter is not
609+
a subtype of the former, since ``List`` is invariant. The responsibility of
610+
writing type-safe type guards is left to the user.
611+
612+
``TypeGuard`` also works with type variables. For more information, see
613+
PEP 647 (User-Defined Type Guards).
614+
"""
615+
item = _type_check(parameters, f'{self} accepts only single type.')
616+
return _GenericAlias(self, (item,))
617+
618+
570619
class ForwardRef(_Final, _root=True):
571620
"""Internal wrapper to hold a forward reference."""
572621

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement :pep:`647` in the :mod:`typing` module by adding
2+
:data:`TypeGuard`.

0 commit comments

Comments
 (0)