bpo-43766: Implement PEP 647 (User-Defined Type Guards) in typing.py (#25282)
diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index c0c6cdd..cb9ba45 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -933,6 +933,80 @@
.. versionadded:: 3.9
+
+.. data:: TypeGuard
+
+ Special typing form 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.
+
+ ``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"::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ 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.
+
+ Using ``-> TypeGuard`` 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``.
+
+ For example::
+
+ def is_str_list(val: List[object]) -> TypeGuard[List[str]]:
+ '''Determines whether all objects in the list are strings'''
+ return all(isinstance(x, str) for x in val)
+
+ def func1(val: List[object]):
+ if is_str_list(val):
+ # Type of ``val`` is narrowed to List[str]
+ print(" ".join(val))
+ else:
+ # 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``.
+
+ 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. Even if
+ the type guard function passes type checks, it may still fail at runtime.
+ The type guard function may perform erroneous checks and return wrong
+ booleans. Consequently, the type it promises in ``TypeGuard[TypeB]`` may
+ not hold.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ :pep:`647` (User-Defined Type Guards).
+
+ .. versionadded:: 3.10
+
+
Building generic types
""""""""""""""""""""""
diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst
index 91ad8ec..eeb0c29 100644
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -743,6 +743,16 @@
(Contributed by Mikhail Golubev in :issue:`41923`.)
+PEP 647: User-Defined Type Guards
+---------------------------------
+
+:data:`TypeGuard` has been added to the :mod:`typing` module to annotate
+type guard functions and improve information provided to static type checkers
+during type narrowing. For more information, please see :data:`TypeGuard`\ 's
+documentation, and :pep:`647`.
+
+(Contributed by Ken Jin and Guido van Rossum in :issue:`43766`.
+PEP written by Eric Traut.)
Other Language Changes
======================
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index c28f390..47dc0b9 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -26,6 +26,7 @@
from typing import Annotated, ForwardRef
from typing import TypeAlias
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
+from typing import TypeGuard
import abc
import typing
import weakref
@@ -4377,6 +4378,45 @@ def test_valid_uses(self):
self.assertEqual(C4.__parameters__, (T, P))
+class TypeGuardTests(BaseTestCase):
+ def test_basics(self):
+ TypeGuard[int] # OK
+
+ def foo(arg) -> TypeGuard[int]: ...
+ self.assertEqual(gth(foo), {'return': TypeGuard[int]})
+
+ def test_repr(self):
+ self.assertEqual(repr(TypeGuard), 'typing.TypeGuard')
+ cv = TypeGuard[int]
+ self.assertEqual(repr(cv), 'typing.TypeGuard[int]')
+ cv = TypeGuard[Employee]
+ self.assertEqual(repr(cv), 'typing.TypeGuard[%s.Employee]' % __name__)
+ cv = TypeGuard[tuple[int]]
+ self.assertEqual(repr(cv), 'typing.TypeGuard[tuple[int]]')
+
+ def test_cannot_subclass(self):
+ with self.assertRaises(TypeError):
+ class C(type(TypeGuard)):
+ pass
+ with self.assertRaises(TypeError):
+ class C(type(TypeGuard[int])):
+ pass
+
+ def test_cannot_init(self):
+ with self.assertRaises(TypeError):
+ TypeGuard()
+ with self.assertRaises(TypeError):
+ type(TypeGuard)()
+ with self.assertRaises(TypeError):
+ type(TypeGuard[Optional[int]])()
+
+ def test_no_isinstance(self):
+ with self.assertRaises(TypeError):
+ isinstance(1, TypeGuard[int])
+ with self.assertRaises(TypeError):
+ issubclass(int, TypeGuard)
+
+
class AllTests(BaseTestCase):
"""Tests for __all__."""
diff --git a/Lib/typing.py b/Lib/typing.py
index 762a98a..d409517 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -119,6 +119,7 @@
'Text',
'TYPE_CHECKING',
'TypeAlias',
+ 'TypeGuard',
]
# The pseudo-submodules 're' and 'io' are part of the public
@@ -567,6 +568,54 @@ def Concatenate(self, parameters):
return _ConcatenateGenericAlias(self, parameters)
+@_SpecialForm
+def TypeGuard(self, parameters):
+ """Special typing form 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.
+
+ ``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".
+
+ 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.
+
+ Using ``-> TypeGuard`` 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``.
+
+ For example::
+
+ def is_str(val: Union[str, float]):
+ # "isinstance" type guard
+ if isinstance(val, str):
+ # Type of ``val`` is narrowed to ``str``
+ ...
+ else:
+ # Else, type of ``val`` is narrowed to ``float``.
+ ...
+
+ Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
+ form of ``TypeA`` (it can even be a wider form) and this may lead to
+ 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.
+
+ ``TypeGuard`` also works with type variables. For more information, see
+ PEP 647 (User-Defined Type Guards).
+ """
+ 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."""
diff --git a/Misc/NEWS.d/next/Library/2021-04-09-00-16-22.bpo-43766.nYNQP0.rst b/Misc/NEWS.d/next/Library/2021-04-09-00-16-22.bpo-43766.nYNQP0.rst
new file mode 100644
index 0000000..4f039a7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-04-09-00-16-22.bpo-43766.nYNQP0.rst
@@ -0,0 +1,2 @@
+Implement :pep:`647` in the :mod:`typing` module by adding
+:data:`TypeGuard`.