]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-43766: Implement PEP 647 (User-Defined Type Guards) in typing.py (#25282)
authorKen Jin <28750310+Fidget-Spinner@users.noreply.github.com>
Tue, 27 Apr 2021 14:31:04 +0000 (22:31 +0800)
committerGitHub <noreply@github.com>
Tue, 27 Apr 2021 14:31:04 +0000 (07:31 -0700)
Doc/library/typing.rst
Doc/whatsnew/3.10.rst
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2021-04-09-00-16-22.bpo-43766.nYNQP0.rst [new file with mode: 0644]

index c0c6cdde221b01bccfe464c037cf27dffb5e7de5..cb9ba4599d7eafd8204f80dd533ae53de8bce0ac 100644 (file)
@@ -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
 """"""""""""""""""""""
 
index 91ad8ec1adb6ebdc5e1f5e7a747ad42196222be1..eeb0c291008e50828538314f6949e7972bfe0ef0 100644 (file)
@@ -743,6 +743,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
 ======================
index c28f390df020752bf6904ff6280e2ccae73247d4..47dc0b9358d7de1c293673e30e38dbebf8cc501d 100644 (file)
@@ -26,6 +26,7 @@ from typing import Pattern, Match
 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 @@ class ConcatenateTests(BaseTestCase):
         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__."""
 
index 762a98ab17193cf4dc4bbe7ad4b26e83c5d7a8dc..d409517ff58e9ae22d0ce7a27d2873d3a52d3e3b 100644 (file)
@@ -119,6 +119,7 @@ __all__ = [
     '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 (file)
index 0000000..4f039a7
--- /dev/null
@@ -0,0 +1,2 @@
+Implement :pep:`647` in the :mod:`typing` module by adding
+:data:`TypeGuard`.