diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index c0c6cdde221b01..cb9ba4599d7eaf 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -933,6 +933,80 @@ These can be used as types in annotations using ``[]``, each having a unique syn .. 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 50c8d53e57d835..bd7286bb52b494 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -578,6 +578,16 @@ See :pep:`613` for more details. (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 82c517a4e60024..2e86d7c4cdd0b3 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 @@ -4364,6 +4365,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 a24c01f0e3b9e3..ab01b9520d51e5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -120,6 +120,7 @@ 'Text', 'TYPE_CHECKING', 'TypeAlias', + 'TypeGuard', ] # The pseudo-submodules 're' and 'io' are part of the public @@ -568,6 +569,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 00000000000000..4f039a7cebbf65 --- /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`.