From afbd42bdf9c795edd776216c56d5bcd29db07bf9 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 9 Apr 2021 00:16:47 +0800 Subject: [PATCH 01/12] Implement PEP 647 --- Doc/library/typing.rst | 49 +++++++++++++++++++ Doc/whatsnew/3.10.rst | 9 ++++ Lib/test/test_typing.py | 49 +++++++++++++++++++ Lib/typing.py | 43 ++++++++++++++++ .../2021-04-09-00-16-22.bpo-43766.nYNQP0.rst | 2 + 5 files changed, 152 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-04-09-00-16-22.bpo-43766.nYNQP0.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index f6d1ccb1c5b3d8..30db805e4e8afa 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -933,6 +933,55 @@ 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. + + ``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, a type guard can be a complex checking function. These functions + are called "type guard functions". These type guard functions require + ``TypeGuard`` to narrow their input types as the static type checker usually + does not have enough information to statically infer them. + + A ``TypeGuard`` tells the static type checker that for a given function: + 1. The return value is a boolean. + 2. If the return value was "truthy", the type of the input to the + function is specified by 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!") + + In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``, + Means that if ``foo(arg)`` returned true, then ``arg`` narrows from + ``TypeA`` to ``TypeB``. + + Return statements within a type guard function should return ``bool`` + values. + + ``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 453a1b42adfa97..8a1f3d343b2401 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -576,6 +576,15 @@ See :pep:`613` for more details. (Contributed by Mikhail Golubev in :issue:`41923`.) +PEP 647: User-Defined Type Guards +----------------------------- + +:data:`TypeGuard` is a new addition to the :mod:`typing` module to annotate +type guard functions and improve information provided to static type checkers +for type narrowing. For more information, please see :data:`TypeGuard`\ 's +documentation, and :pep:`647`. + +(Contributed by Ken Jin 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 3b8efe16c6e238..4387601ce6bc42 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 +from typing import TypeGuard import abc import typing import weakref @@ -4346,6 +4347,54 @@ 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]}) + + with self.assertRaises(TypeError): + TypeGuard[1] + with self.assertRaises(TypeError): + TypeGuard[int, str] + + 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) + + def test_final_unmodified(self): + def func(x): ... + self.assertIs(func, final(func)) + + class AllTests(BaseTestCase): """Tests for __all__.""" diff --git a/Lib/typing.py b/Lib/typing.py index 6224930c3b0275..50f1d6fb01f72d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -118,6 +118,7 @@ 'Text', 'TYPE_CHECKING', 'TypeAlias', + 'TypeGuard', ] # The pseudo-submodules 're' and 'io' are part of the public @@ -566,6 +567,48 @@ 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. + + ``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. + + A ``TypeGuard`` tells the static type checker that for a given function: + 1. The return value is a boolean. + 2. If the return value was "truthy", the type of the input to the + function is specified by 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!") + + In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``, + Means that if ``foo(arg)`` returned true, then ``arg`` narrows from + ``TypeA`` to ``TypeB``. + + Return statements within a type guard function should return ``bool`` + values. + + ``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`. From 8c994a5c99111be026ddbf6dcf558baa2af91c47 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 9 Apr 2021 00:26:29 +0800 Subject: [PATCH 02/12] improve docs --- Doc/library/typing.rst | 8 ++++---- Doc/whatsnew/3.10.rst | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 30db805e4e8afa..7cc65f84391ec4 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -945,10 +945,10 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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, a type guard can be a complex checking function. These functions - are called "type guard functions". These type guard functions require - ``TypeGuard`` to narrow their input types as the static type checker usually - does not have enough information to statically infer them. + Sometimes, a type guard can be a complex checking function. + These type guard functions require ``TypeGuard`` to narrow their input types + as the static type checker usually does not have enough information to + statically infer them. A ``TypeGuard`` tells the static type checker that for a given function: 1. The return value is a boolean. diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 8a1f3d343b2401..3ce6208ac52499 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -577,11 +577,11 @@ See :pep:`613` for more details. (Contributed by Mikhail Golubev in :issue:`41923`.) PEP 647: User-Defined Type Guards ------------------------------ +--------------------------------- -:data:`TypeGuard` is a new addition to the :mod:`typing` module to annotate +:data:`TypeGuard` has been added to the :mod:`typing` module to annotate type guard functions and improve information provided to static type checkers -for type narrowing. For more information, please see :data:`TypeGuard`\ 's +during type narrowing. For more information, please see :data:`TypeGuard`\ 's documentation, and :pep:`647`. (Contributed by Ken Jin in :issue:`43766`. PEP written by Eric Traut.) From 493b18d3629f660ac7977ed681ce2fc0e6a0160b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 9 Apr 2021 00:30:34 +0800 Subject: [PATCH 03/12] delete redundant test --- Lib/test/test_typing.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 4387601ce6bc42..0b8247dc70ded1 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4390,10 +4390,6 @@ def test_no_isinstance(self): with self.assertRaises(TypeError): issubclass(int, TypeGuard) - def test_final_unmodified(self): - def func(x): ... - self.assertIs(func, final(func)) - class AllTests(BaseTestCase): """Tests for __all__.""" From eb8dc6b5848a5a0ee74fe1179fd592172af52cab Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 09:01:26 +0800 Subject: [PATCH 04/12] Address some of Guido's doc comments Co-Authored-By: Guido van Rossum --- Doc/library/typing.rst | 39 ++++++++++++++++++++++++++++----------- Lib/typing.py | 36 +++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 7cc65f84391ec4..e3579d56ba760f 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -938,22 +938,40 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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 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". + conditional expression here is sometimes referred to as a "type guard":: + + def func(val: Optional[Union[str, float]]): + # Non-"None" type guard + if val is not None: + # Type of val is narrowed to ``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``. + ... + else: + # Type of val remains Optional[Union[str, float]] + ... - Sometimes, a type guard can be a complex checking function. - These type guard functions require ``TypeGuard`` to narrow their input types - as the static type checker usually does not have enough information to - statically infer them. + Sometimes, a type guard uses a user-defined checking function instead of + ``isinstance`` or ``is None`` checks. These user-defined type guard + functions require ``TypeGuard`` to narrow their input types as the static + type checker usually does not have enough information to statically infer + them. - A ``TypeGuard`` tells the static type checker that for a given function: + Using ``-> TypeGuard`` tells the static type checker that for a given + function: 1. The return value is a boolean. - 2. If the return value was "truthy", the type of the input to the - function is specified by the type inside ``TypeGuard``. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. For example:: @@ -970,11 +988,10 @@ These can be used as types in annotations using ``[]``, each having a unique syn print("Not a list of strings!") In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``, - Means that if ``foo(arg)`` returned true, then ``arg`` narrows from + means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from ``TypeA`` to ``TypeB``. - Return statements within a type guard function should return ``bool`` - values. + A type guard function should return a ``bool`` value. ``TypeGuard`` also works with type variables. For more information, see :pep:`647` (User-Defined Type Guards). diff --git a/Lib/typing.py b/Lib/typing.py index 50f1d6fb01f72d..6c92c2ac52d32c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -570,16 +570,26 @@ def Concatenate(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. - - ``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. - - A ``TypeGuard`` tells the static type checker that for a given function: + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way 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, a type guard uses a user-defined checking function instead of + ``isinstance`` or ``is None`` checks. These user-defined type guard + functions require ``TypeGuard`` to narrow their input types as the static + type checker usually does not have enough information to statically infer + them. + + Using ``-> TypeGuard`` tells the static type checker that for a given + function: 1. The return value is a boolean. - 2. If the return value was "truthy", the type of the input to the - function is specified by the type inside ``TypeGuard``. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. For example:: @@ -596,14 +606,14 @@ def func1(val: List[object]): print("Not a list of strings!") In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``, - Means that if ``foo(arg)`` returned true, then ``arg`` narrows from + means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from ``TypeA`` to ``TypeB``. - Return statements within a type guard function should return ``bool`` - values. + A type guard function should return a ``bool`` value. ``TypeGuard`` also works with type variables. For more information, see - :pep:`647` (User-Defined Type Guards). + PEP 647 (User-Defined Type Guards). + """ item = _type_check(parameters, f'{self} accepts only single type.') return _GenericAlias(self, (item,)) From 49964fa3c5945900f54294e5936ec80d81dfdd6d Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 09:02:16 +0800 Subject: [PATCH 05/12] small typo --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 6c92c2ac52d32c..268096513b611e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -577,7 +577,7 @@ def TypeGuard(self, parameters): 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":: + conditional expression here is sometimes referred to as a "type guard". Sometimes, a type guard uses a user-defined checking function instead of ``isinstance`` or ``is None`` checks. These user-defined type guard From fa736a7dd6e15681a0ce7f1a6fc1f68887fa242b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 09:21:30 +0800 Subject: [PATCH 06/12] Fix doc builder failures, add comment about no strict narrowing --- Doc/library/typing.rst | 16 +++++++++++++--- Lib/typing.py | 26 ++++++++++++++------------ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index e3579d56ba760f..24716f2d5e28aa 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -969,9 +969,10 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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``. + + 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:: @@ -991,6 +992,15 @@ These can be used as types in annotations using ``[]``, each having a unique syn means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from ``TypeA`` to ``TypeB``. +.. note:: + Type checkers and the Python runtime do not enforce strict narrowing. + ``TypeB`` need not be a narrower form of ``TypeA`` + (it can even be a wider form) and this can lead to type-unsafe results. + Strict narrowing is unenforced to not burden the user with invariance rules + or type compatibility with other types. This allows greater articulation of + the subtleties in type guards. The responsibility of writing type-safe + type guards is left to the user. + A type guard function should return a ``bool`` value. ``TypeGuard`` also works with type variables. For more information, see diff --git a/Lib/typing.py b/Lib/typing.py index 268096513b611e..00be0e39570e46 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -570,14 +570,14 @@ def Concatenate(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 return a boolean. + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way 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". + ``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, a type guard uses a user-defined checking function instead of ``isinstance`` or ``is None`` checks. These user-defined type guard @@ -587,9 +587,10 @@ def TypeGuard(self, parameters): 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``. + + 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:: @@ -607,13 +608,14 @@ def func1(val: List[object]): In short, the form ``def foo(arg: TypeA) -> TypeGuard[TypeB]: ...``, means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from - ``TypeA`` to ``TypeB``. + ``TypeA`` to ``TypeB``. Note that strict type narrowing is not enforced - + ``TypeB`` need not be a narrower form of ``TypeB``. The responsibility of + writing type-safe type guards is left to the user. A type guard function should return a ``bool`` value. ``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,)) From 5e4c5f7010e0117ee22ee420bef476fd9b8a1277 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 09:24:22 +0800 Subject: [PATCH 07/12] relax tests for arguments --- Lib/test/test_typing.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 0b8247dc70ded1..34653ac24f919e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4354,11 +4354,6 @@ def test_basics(self): def foo(arg) -> TypeGuard[int]: ... self.assertEqual(gth(foo), {'return': TypeGuard[int]}) - with self.assertRaises(TypeError): - TypeGuard[1] - with self.assertRaises(TypeError): - TypeGuard[int, str] - def test_repr(self): self.assertEqual(repr(TypeGuard), 'typing.TypeGuard') cv = TypeGuard[int] From 481b618dad81b213443cc291e3cf9466e1110032 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 10:40:48 +0800 Subject: [PATCH 08/12] fix indentation, improve wording about unenforced narrowing --- Doc/library/typing.rst | 21 ++++++++++----------- Lib/typing.py | 6 ++---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 24716f2d5e28aa..ab739b90a6b7c0 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -938,7 +938,7 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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 return a boolean. + 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 @@ -992,16 +992,15 @@ These can be used as types in annotations using ``[]``, each having a unique syn means that if ``foo(arg)`` returns ``True``, then ``arg`` narrows from ``TypeA`` to ``TypeB``. -.. note:: - Type checkers and the Python runtime do not enforce strict narrowing. - ``TypeB`` need not be a narrower form of ``TypeA`` - (it can even be a wider form) and this can lead to type-unsafe results. - Strict narrowing is unenforced to not burden the user with invariance rules - or type compatibility with other types. This allows greater articulation of - the subtleties in type guards. The responsibility of writing type-safe - type guards is left to the user. - - A type guard function should return a ``bool`` value. + .. note:: + + 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 intent is to not burden the user with invariance rules + and type compatibility with other types. This allows for greater + expression of the subtleties in type guards. 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). diff --git a/Lib/typing.py b/Lib/typing.py index 00be0e39570e46..8da1e3fa7c2a4c 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -571,7 +571,7 @@ def Concatenate(self, parameters): 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 return a boolean. + 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 @@ -609,11 +609,9 @@ def func1(val: List[object]): 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 that strict type narrowing is not enforced - - ``TypeB`` need not be a narrower form of ``TypeB``. The responsibility of + ``TypeB`` need not be a narrower form of ``TypeA``. The responsibility of writing type-safe type guards is left to the user. - A type guard function should return a ``bool`` value. - ``TypeGuard`` also works with type variables. For more information, see PEP 647 (User-Defined Type Guards). """ From 7f3a11339048c1c379359025b87aa0e3ee3d84f4 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 10:52:52 +0800 Subject: [PATCH 09/12] Add note about class/instance methods, improve wording of type narrowing enforcement --- Doc/library/typing.rst | 11 +++++++---- Lib/typing.py | 9 +++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index ab739b90a6b7c0..c60c3f337ee267 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -988,6 +988,10 @@ These can be used as types in annotations using ``[]``, each having a unique syn # 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``. @@ -996,10 +1000,9 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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 intent is to not burden the user with invariance rules - and type compatibility with other types. This allows for greater - expression of the subtleties in type guards. The responsibility of + type-unsafe results. The main reason is to allow for things like + narrowing ``List[object]`` to ``List[str]`` which would fail under strict + narrowing as ``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 diff --git a/Lib/typing.py b/Lib/typing.py index 8da1e3fa7c2a4c..5d45b3b8a4eac1 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -606,10 +606,11 @@ def func1(val: List[object]): # Type of ``val`` remains as List[object] print("Not a list of strings!") - 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 that strict type narrowing is not enforced - - ``TypeB`` need not be a narrower form of ``TypeA``. The responsibility of + 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]`` which would fail under strict + narrowing as ``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 From f015c8223a78c9239408fbff3634d4d607faeb06 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 11:23:35 +0800 Subject: [PATCH 10/12] inform the user that typeguard may fail at runtime even after passing type checks --- Doc/library/typing.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 51e09574dec326..d25a744ea7d0c2 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1003,7 +1003,11 @@ These can be used as types in annotations using ``[]``, each having a unique syn type-unsafe results. The main reason is to allow for things like narrowing ``List[object]`` to ``List[str]`` which would fail under strict narrowing as ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. + writing type-safe type guards is left to the user. Furthermore, 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). From 6731b24e33eb14e787381bec560b09cef032a243 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 11:33:53 +0800 Subject: [PATCH 11/12] Address Guido's docs reviews Co-Authored-By: Guido van Rossum --- Doc/library/typing.rst | 38 +++++++++++++++----------------------- Doc/whatsnew/3.10.rst | 3 ++- Lib/typing.py | 29 ++++++++++++----------------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index d25a744ea7d0c2..66fefb75a02881 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -946,26 +946,18 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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 func(val: Optional[Union[str, float]]): - # Non-"None" type guard - if val is not None: - # Type of val is narrowed to ``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``. - ... + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... else: - # Type of val remains Optional[Union[str, float]] + # Else, type of ``val`` is narrowed to ``float``. ... - Sometimes, a type guard uses a user-defined checking function instead of - ``isinstance`` or ``is None`` checks. These user-defined type guard - functions require ``TypeGuard`` to narrow their input types as the static - type checker usually does not have enough information to statically infer - them. + 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: @@ -998,12 +990,12 @@ These can be used as types in annotations using ``[]``, each having a unique syn .. note:: - 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]`` which would fail under strict - narrowing as ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. Furthermore, even if + ``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 diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 7d9b88ec9c9a5e..bd7286bb52b494 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -586,7 +586,8 @@ 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 in :issue:`43766`. PEP written by Eric Traut.) +(Contributed by Ken Jin and Guido van Rossum in :issue:`43766`. +PEP written by Eric Traut.) Other Language Changes ====================== diff --git a/Lib/typing.py b/Lib/typing.py index 0316ef314c8c56..fa7ba1c800e545 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -581,11 +581,9 @@ def TypeGuard(self, parameters): 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, a type guard uses a user-defined checking function instead of - ``isinstance`` or ``is None`` checks. These user-defined type guard - functions require ``TypeGuard`` to narrow their input types as the static - type checker usually does not have enough information to statically infer - them. + 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: @@ -596,23 +594,20 @@ def TypeGuard(self, parameters): 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)) + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... else: - # Type of ``val`` remains as List[object] - print("Not a list of strings!") + # 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]`` which would fail under strict - narrowing as ``List`` is invariant. The responsibility of + 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 From 39e8d8e54318715e09b6293e8f141971e64b96cf Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 11 Apr 2021 16:41:18 +0800 Subject: [PATCH 12/12] fix formatting --- Doc/library/typing.rst | 4 ++-- Lib/typing.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 66fefb75a02881..cb9ba4599d7eaf 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -940,7 +940,7 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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 + ``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 @@ -998,7 +998,7 @@ These can be used as types in annotations using ``[]``, each having a unique syn 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 + booleans. Consequently, the type it promises in ``TypeGuard[TypeB]`` may not hold. ``TypeGuard`` also works with type variables. For more information, see diff --git a/Lib/typing.py b/Lib/typing.py index fa7ba1c800e545..ab01b9520d51e5 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -575,7 +575,7 @@ def TypeGuard(self, parameters): 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 + ``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 @@ -603,7 +603,7 @@ def is_str(val: Union[str, float]): # Else, type of ``val`` is narrowed to ``float``. ... - Strict type narrowing is not enforced - ``TypeB`` need not be a narrower + 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