From 3fa82def307b951f6d71644a1467b8c47a315b68 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 3 Apr 2024 14:48:16 -0400 Subject: [PATCH 01/10] gh-117516: Implement typing.TypeIs --- Doc/library/typing.rst | 135 +++++++++++++++++++++++++++++----------- Doc/whatsnew/3.13.rst | 4 ++ Lib/test/test_typing.py | 56 ++++++++++++++++- Lib/typing.py | 82 ++++++++++++++++++++++-- 4 files changed, 237 insertions(+), 40 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 73214e18d556b2..e45561b67625ae 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1389,11 +1389,61 @@ These can be used as types in annotations. They all support subscription using Special typing construct for marking user-defined type guard functions. - ``TypeGuard`` can be 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. + Type guard functions are user-defined functions that return whether their + argument is an instance of a particular type. + ``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different + effects on type checking behavior. - ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static + 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!") + + ``TypeIs`` and ``TypeGuard`` differ in the following ways: + + * ``TypeIs`` requires the narrowed type to be a subtype of the input type, while + ``TypeGuard`` does not. 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. + * When a ``TypeGuard`` function returns ``True``, type checkers narrow the type of the + variable to exactly the ``TypeGuard`` type. When a ``TypeIs`` function returns ``True``, + type checkers can infer a more precise type combining the previously known type of the + variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.) + * When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of + the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow + the type of the variable to exclude the ``TypeIs`` type. + + ``TypeGuard`` also works with type variables. See :pep:`647` for more details. + + .. versionadded:: 3.10 + + +.. data:: TypeIs + + Special typing construct for marking user-defined type narrowing functions. + + ``TypeIs`` can be used to annotate the return type of a user-defined + type guard function. ``TypeIs`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean and take at + least one positional argument. + + ``TypeIs`` 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 @@ -1409,49 +1459,64 @@ These can be used as types in annotations. They all support subscription using ... 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 + as a type guard. Such a function should use ``TypeIs[...]`` or + :data:`TypeGuard` as its return type to alert static type checkers to + this intention. ``TypeIs`` usually has more intuitive behavior than + ``TypeGuard``, but it cannot be used when the input and output types + are incompatible (e.g., ``list[object]`` to ``list[int]``) and when the + function does not return ``True`` for all instances of the narrowed type. + + Using ``-> TypeIs`` 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``. + is the intersection of the argument's original type and the + type inside ``TypeIs``. + 3. If the return value is ``False``, the type of its argument + is narrowed to exclude the type inside ``TypeIs``. 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 + from typing import assert_type, final, TypeIs + + class Parent: pass + class Child(Parent): pass + @final + class Unrelated: pass + + def is_parent(val: object) -> TypeIs[Parent]: + return isinstance(val, Parent) + + def run(arg: Child | Unrelated): + if is_parent(arg): + # Type of ``arg`` is narrowed to the intersection + # of ``Parent`` and ``Child``, which is equivalent to + # ``Child``. + assert_type(arg, Child) + else: + # Type of ``arg`` is narrowed to exclude ``Parent``, + # so only ``Unrelated`` is left. + assert_type(arg, Unrelated) + + The type inside ``TypeIs`` must be consistent with the type of the + function's argument; if it is not, static type checkers will raise + an error. An incorrectly written ``TypeIs`` function can lead to + unsound behavior in the type system; it is the user's responsibility + to write such functions in a type-safe manner. + + If a ``TypeIs`` function 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``. + In short, the form ``def foo(arg: TypeA) -> TypeIs[TypeB]: ...``, + means that if ``foo(arg)`` returns ``True``, then ``arg`` is an instance + of ``TypeB``, and if it returns ``False``, it is not an instance of ``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. - - ``TypeGuard`` also works with type variables. See :pep:`647` for more details. + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with ``TypeIs``). - .. versionadded:: 3.10 + .. versionadded:: 3.13 .. data:: Unpack diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 7f6a86efc61bf7..3ec71517b6128e 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -87,6 +87,10 @@ Interpreter improvements: Performance improvements are modest -- we expect to be improving this over the next few releases. +New typing features: + +* :pep:`742`: :data:`typing.TypeIs` was added, providing more intuitive + type narrowing behavior. New Features ============ diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 927f74eb69fbc7..bae0a8480b994f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -38,7 +38,7 @@ from typing import Self, LiteralString from typing import TypeAlias from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs -from typing import TypeGuard +from typing import TypeGuard, TypeIs import abc import textwrap import typing @@ -5207,6 +5207,7 @@ def test_subclass_special_form(self): Literal[1, 2], Concatenate[int, ParamSpec("P")], TypeGuard[int], + TypeIs[range], ): with self.subTest(msg=obj): with self.assertRaisesRegex( @@ -6748,6 +6749,7 @@ class C(Generic[T]): pass self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(TypeAlias), ()) self.assertEqual(get_args(TypeGuard[int]), (int,)) + self.assertEqual(get_args(TypeIs[range]), (range,)) Ts = TypeVarTuple('Ts') self.assertEqual(get_args(Ts), ()) self.assertEqual(get_args((*Ts,)[0]), (Ts,)) @@ -9592,6 +9594,56 @@ def test_no_isinstance(self): issubclass(int, TypeGuard) +class TypeIsTests(BaseTestCase): + def test_basics(self): + TypeIs[int] # OK + + def foo(arg) -> TypeIs[int]: ... + self.assertEqual(gth(foo), {'return': TypeIs[int]}) + + with self.assertRaises(TypeError): + TypeIs[int, str] + + def test_repr(self): + self.assertEqual(repr(TypeIs), 'typing.TypeIs') + cv = TypeIs[int] + self.assertEqual(repr(cv), 'typing.TypeIs[int]') + cv = TypeIs[Employee] + self.assertEqual(repr(cv), 'typing.TypeIs[%s.Employee]' % __name__) + cv = TypeIs[tuple[int]] + self.assertEqual(repr(cv), 'typing.TypeIs[tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class C(type(TypeIs)): + pass + with self.assertRaisesRegex(TypeError, CANNOT_SUBCLASS_TYPE): + class D(type(TypeIs[int])): + pass + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeIs'): + class E(TypeIs): + pass + with self.assertRaisesRegex(TypeError, + r'Cannot subclass typing\.TypeIs\[int\]'): + class F(TypeIs[int]): + pass + + def test_cannot_init(self): + with self.assertRaises(TypeError): + TypeIs() + with self.assertRaises(TypeError): + type(TypeIs)() + with self.assertRaises(TypeError): + type(TypeIs[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeIs[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeIs) + + SpecialAttrsP = typing.ParamSpec('SpecialAttrsP') SpecialAttrsT = typing.TypeVar('SpecialAttrsT', int, float, complex) @@ -9691,6 +9743,7 @@ def test_special_attrs(self): typing.Optional: 'Optional', typing.TypeAlias: 'TypeAlias', typing.TypeGuard: 'TypeGuard', + typing.TypeIs: 'TypeIs', typing.TypeVar: 'TypeVar', typing.Union: 'Union', typing.Self: 'Self', @@ -9705,6 +9758,7 @@ def test_special_attrs(self): typing.Literal[True, 2]: 'Literal', typing.Optional[Any]: 'Optional', typing.TypeGuard[Any]: 'TypeGuard', + typing.TypeIs[Any]: 'TypeIs', typing.Union[Any]: 'Any', typing.Union[int, float]: 'Union', # Incompatible special forms (tested in test_special_attrs2) diff --git a/Lib/typing.py b/Lib/typing.py index ef532f6c91539d..297d2ab6bc32ac 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -153,6 +153,7 @@ 'TYPE_CHECKING', 'TypeAlias', 'TypeGuard', + 'TypeIs', 'TypeAliasType', 'Unpack', ] @@ -831,8 +832,11 @@ def TypeGuard(self, parameters): 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. + as a type guard. Such a function should use ``TypeGuard[...]`` or + ``TypeIs[...]`` as its return type to alert static type checkers to + this intention. ``TypeGuard`` should be used over ``TypeIs`` when narrowing + from an incompatible type (e.g., ``list[object]`` to ``list[int]``) or when + the function does not return ``True`` for all instances of the narrowed type. Using ``-> TypeGuard`` tells the static type checker that for a given function: @@ -866,6 +870,75 @@ def is_str(val: Union[str, float]): return _GenericAlias(self, (item,)) +@_SpecialForm +def TypeIs(self, parameters): + """Special typing construct for marking user-defined type narrowing functions. + + ``TypeIs`` can be used to annotate the return type of a user-defined + type guard function. ``TypeIs`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean and accept + at least one argument. + + ``TypeIs`` 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 ``TypeIs[...]`` or + ``TypeGuard[...]`` as its return type to alert static type checkers to + this intention. ``TypeIs`` usually has more intuitive behavior than + ``TypeGuard``, but it cannot be used when the input and output types + are incompatible (e.g., ``list[object]`` to ``list[int]``) and when the + function does not return ``True`` for all instances of the narrowed type. + + Using ``-> TypeIs`` 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 intersection of the argument's original type and the + type inside ``TypeIs``. + 3. If the return value is ``False``, the type of its argument + is narrowed to exclude the type inside ``TypeIs``. + + For example:: + + from typing import assert_type, final, TypeIs + + class Parent: pass + class Child(Parent): pass + @final + class Unrelated: pass + + def is_parent(val: object) -> TypeIs[Parent]: + return isinstance(val, Parent) + + def run(arg: Child | Unrelated): + if is_parent(arg): + # Type of ``arg`` is narrowed to the intersection + # of ``Parent`` and ``Child``, which is equivalent to + # ``Child``. + assert_type(arg, Child) + else: + # Type of ``arg`` is narrowed to exclude ``Parent``, + # so only ``Unrelated`` is left. + assert_type(arg, Unrelated) + + The type inside ``TypeIs`` must be consistent with the type of the + function's argument; if it is not, static type checkers will raise + an error. An incorrectly written ``TypeIs`` function can lead to + unsound behavior in the type system; it is the user's responsibility + to write such functions in a type-safe manner. + + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with ``TypeIs``). + """ + 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.""" @@ -1238,11 +1311,12 @@ class _GenericAlias(_BaseGenericAlias, _root=True): # A = Callable[[], None] # _CallableGenericAlias # B = Callable[[T], None] # _CallableGenericAlias # C = B[int] # _CallableGenericAlias - # * Parameterized `Final`, `ClassVar` and `TypeGuard`: + # * Parameterized `Final`, `ClassVar`, `TypeGuard`, and `TypeIs`: # # All _GenericAlias # Final[int] # ClassVar[float] - # TypeVar[bool] + # TypeGuard[bool] + # TypeIs[range] def __init__(self, origin, args, *, inst=True, name=None): super().__init__(origin, inst=inst, name=name) From 99815d0a1c056e99f53283e09aa7e014494b1ef8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 3 Apr 2024 16:01:36 -0400 Subject: [PATCH 02/10] blurb --- .../next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst diff --git a/Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst b/Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst new file mode 100644 index 00000000000000..bbf69126d956d2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-03-16-01-31.gh-issue-117516.7DlHje.rst @@ -0,0 +1 @@ +Add :data:`typing.TypeIs`, implementing :pep:`742`. Patch by Jelle Zijlstra. From 5c8732a9397b403e1e163a320e68a88b504ff06d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 4 Apr 2024 06:43:58 -0400 Subject: [PATCH 03/10] Type predicates --- Doc/library/typing.rst | 14 +++++++------- Lib/typing.py | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index e45561b67625ae..5c82d3a96c35bb 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1387,9 +1387,9 @@ These can be used as types in annotations. They all support subscription using .. data:: TypeGuard - Special typing construct for marking user-defined type guard functions. + Special typing construct for marking user-defined type predicate functions. - Type guard functions are user-defined functions that return whether their + Type predicate functions are user-defined functions that return whether their argument is an instance of a particular type. ``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different effects on type checking behavior. @@ -1436,10 +1436,10 @@ These can be used as types in annotations. They all support subscription using .. data:: TypeIs - Special typing construct for marking user-defined type narrowing functions. + Special typing construct for marking user-defined type predicate functions. ``TypeIs`` can be used to annotate the return type of a user-defined - type guard function. ``TypeIs`` only accepts a single type argument. + type predicate function. ``TypeIs`` only accepts a single type argument. At runtime, functions marked this way should return a boolean and take at least one positional argument. @@ -1447,10 +1447,10 @@ These can be used as types in annotations. They all support subscription using 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 predicate":: def is_str(val: str | float): - # "isinstance" type guard + # "isinstance" type predicate if isinstance(val, str): # Type of ``val`` is narrowed to ``str`` ... @@ -1459,7 +1459,7 @@ These can be used as types in annotations. They all support subscription using ... Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeIs[...]`` or + as a type predicate. Such a function should use ``TypeIs[...]`` or :data:`TypeGuard` as its return type to alert static type checkers to this intention. ``TypeIs`` usually has more intuitive behavior than ``TypeGuard``, but it cannot be used when the input and output types diff --git a/Lib/typing.py b/Lib/typing.py index 7811bb21766f59..e6833e57ff0114 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -819,20 +819,20 @@ def Concatenate(self, parameters): @_SpecialForm def TypeGuard(self, parameters): - """Special typing construct for marking user-defined type guard functions. + """Special typing construct for marking user-defined type predicate functions. ``TypeGuard`` can be used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. + type predicate 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". + conditional expression here is sometimes referred to as a "type predicate". Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeGuard[...]`` or + as a type predicate. Such a function should use ``TypeGuard[...]`` or ``TypeIs[...]`` as its return type to alert static type checkers to this intention. ``TypeGuard`` should be used over ``TypeIs`` when narrowing from an incompatible type (e.g., ``list[object]`` to ``list[int]``) or when @@ -864,7 +864,7 @@ def func1(val: list[object]): 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. + writing type-safe type predicates is left to the user. ``TypeGuard`` also works with type variables. For more information, see PEP 647 (User-Defined Type Guards). @@ -875,10 +875,10 @@ def func1(val: list[object]): @_SpecialForm def TypeIs(self, parameters): - """Special typing construct for marking user-defined type narrowing functions. + """Special typing construct for marking user-defined type predicate functions. ``TypeIs`` can be used to annotate the return type of a user-defined - type guard function. ``TypeIs`` only accepts a single type argument. + type predicate function. ``TypeIs`` only accepts a single type argument. At runtime, functions marked this way should return a boolean and accept at least one argument. @@ -886,14 +886,14 @@ def TypeIs(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 predicate". Sometimes it would be convenient to use a user-defined boolean function - as a type guard. Such a function should use ``TypeIs[...]`` or + as a type predicate. Such a function should use ``TypeIs[...]`` or ``TypeGuard[...]`` as its return type to alert static type checkers to this intention. ``TypeIs`` usually has more intuitive behavior than ``TypeGuard``, but it cannot be used when the input and output types - are incompatible (e.g., ``list[object]`` to ``list[int]``) and when the + are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the function does not return ``True`` for all instances of the narrowed type. Using ``-> TypeIs`` tells the static type checker that for a given From ffe27c15972625d29bc1f141b5bd423cde501c6e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 4 Apr 2024 06:50:22 -0400 Subject: [PATCH 04/10] Reorder --- Doc/library/typing.rst | 98 +++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 5c82d3a96c35bb..bb306541608364 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1385,55 +1385,6 @@ These can be used as types in annotations. They all support subscription using .. versionadded:: 3.9 -.. data:: TypeGuard - - Special typing construct for marking user-defined type predicate functions. - - Type predicate functions are user-defined functions that return whether their - argument is an instance of a particular type. - ``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different - effects on type checking behavior. - - 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!") - - ``TypeIs`` and ``TypeGuard`` differ in the following ways: - - * ``TypeIs`` requires the narrowed type to be a subtype of the input type, while - ``TypeGuard`` does not. 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. - * When a ``TypeGuard`` function returns ``True``, type checkers narrow the type of the - variable to exactly the ``TypeGuard`` type. When a ``TypeIs`` function returns ``True``, - type checkers can infer a more precise type combining the previously known type of the - variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.) - * When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of - the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow - the type of the variable to exclude the ``TypeIs`` type. - - ``TypeGuard`` also works with type variables. See :pep:`647` for more details. - - .. versionadded:: 3.10 - - .. data:: TypeIs Special typing construct for marking user-defined type predicate functions. @@ -1519,6 +1470,55 @@ These can be used as types in annotations. They all support subscription using .. versionadded:: 3.13 +.. data:: TypeGuard + + Special typing construct for marking user-defined type predicate functions. + + Type predicate functions are user-defined functions that return whether their + argument is an instance of a particular type. + ``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different + effects on type checking behavior. + + 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!") + + ``TypeIs`` and ``TypeGuard`` differ in the following ways: + + * ``TypeIs`` requires the narrowed type to be a subtype of the input type, while + ``TypeGuard`` does not. 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. + * When a ``TypeGuard`` function returns ``True``, type checkers narrow the type of the + variable to exactly the ``TypeGuard`` type. When a ``TypeIs`` function returns ``True``, + type checkers can infer a more precise type combining the previously known type of the + variable with the ``TypeIs`` type. (Technically, this is known as an intersection type.) + * When a ``TypeGuard`` function returns ``False``, type checkers cannot narrow the type of + the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow + the type of the variable to exclude the ``TypeIs`` type. + + ``TypeGuard`` also works with type variables. See :pep:`647` for more details. + + .. versionadded:: 3.10 + + .. data:: Unpack Typing operator to conceptually mark an object as having been unpacked. From d32b8ab0a595397642659b865cac1aa7952f9988 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 4 Apr 2024 06:53:23 -0400 Subject: [PATCH 05/10] PEP link --- Doc/library/typing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index bb306541608364..8fdf400b7f4c3e 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1465,7 +1465,7 @@ These can be used as types in annotations. They all support subscription using of ``TypeB``, and if it returns ``False``, it is not an instance of ``TypeB``. ``TypeIs`` also works with type variables. For more information, see - PEP 742 (Narrowing types with ``TypeIs``). + :pep:`742` (Narrowing types with ``TypeIs``). .. versionadded:: 3.13 From 1b6e77145b7555bec0368de25453a91654c4e863 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 6 Apr 2024 08:52:07 -0400 Subject: [PATCH 06/10] Update Doc/library/typing.rst Co-authored-by: Carl Meyer --- Doc/library/typing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 8fdf400b7f4c3e..ef6f1bfce2c155 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1414,7 +1414,7 @@ These can be used as types in annotations. They all support subscription using :data:`TypeGuard` as its return type to alert static type checkers to this intention. ``TypeIs`` usually has more intuitive behavior than ``TypeGuard``, but it cannot be used when the input and output types - are incompatible (e.g., ``list[object]`` to ``list[int]``) and when the + are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the function does not return ``True`` for all instances of the narrowed type. Using ``-> TypeIs`` tells the static type checker that for a given From 86800c4fa0d8cb0c416f02699be1c887a491dd25 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 6 Apr 2024 08:54:57 -0400 Subject: [PATCH 07/10] Suggestion from Carl --- Lib/typing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index e6833e57ff0114..231492cdcc01cf 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -838,12 +838,12 @@ def TypeGuard(self, parameters): from an incompatible type (e.g., ``list[object]`` to ``list[int]``) or when the function does not return ``True`` for all instances of the narrowed type. - Using ``-> TypeGuard`` tells the static type checker that for a given - function: + Using ``-> TypeGuard[NarrowedType]`` 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``. + is ``NarrowedType``. For example:: @@ -896,15 +896,15 @@ def TypeIs(self, parameters): are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the function does not return ``True`` for all instances of the narrowed type. - Using ``-> TypeIs`` tells the static type checker that for a given - function: + Using ``-> TypeIs[NarrowedType]`` 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 intersection of the argument's original type and the - type inside ``TypeIs``. + is the intersection of the argument's original type and + ``NarrowedType``. 3. If the return value is ``False``, the type of its argument - is narrowed to exclude the type inside ``TypeIs``. + is narrowed to exclude ``NarrowedType``. For example:: From f1f16018290893d1148941bcf6200801b50187c9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 6 Apr 2024 09:34:18 -0400 Subject: [PATCH 08/10] Forgot to save --- Doc/library/typing.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index ef6f1bfce2c155..3b60a141e96da2 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1417,15 +1417,14 @@ These can be used as types in annotations. They all support subscription using are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the function does not return ``True`` for all instances of the narrowed type. - Using ``-> TypeIs`` tells the static type checker that for a given + Using ``-> TypeIs[NarrowedType]`` 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 intersection of the argument's original type and the - type inside ``TypeIs``. + is the intersection of the argument's original type and ``NarrowedType``. 3. If the return value is ``False``, the type of its argument - is narrowed to exclude the type inside ``TypeIs``. + is narrowed to exclude ``NarrowedType``. For example:: From d28ad785c39f3e731fbb7b394e290b631712b55d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 8 Apr 2024 22:06:50 -0400 Subject: [PATCH 09/10] Update Doc/library/typing.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/typing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 3b60a141e96da2..534ae7882b197f 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1456,7 +1456,7 @@ These can be used as types in annotations. They all support subscription using to write such functions in a type-safe manner. If a ``TypeIs`` function is a class or instance method, then the type in - ``TypeGuard`` maps to the type of the second parameter after ``cls`` or + ``TypeIs`` maps to the type of the second parameter after ``cls`` or ``self``. In short, the form ``def foo(arg: TypeA) -> TypeIs[TypeB]: ...``, From 5bf8126a9455886226a670e35bb31d974c85acdd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 9 Apr 2024 06:17:21 -0400 Subject: [PATCH 10/10] More PR feedback --- Doc/library/typing.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 534ae7882b197f..19dbd376c80d51 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1476,7 +1476,7 @@ These can be used as types in annotations. They all support subscription using Type predicate functions are user-defined functions that return whether their argument is an instance of a particular type. ``TypeGuard`` works similarly to :data:`TypeIs`, but has subtly different - effects on type checking behavior. + effects on type checking behavior (see below). Using ``-> TypeGuard`` tells the static type checker that for a given function: @@ -1485,6 +1485,8 @@ These can be used as types in annotations. They all support subscription using 2. If the return value is ``True``, the type of its argument is the type inside ``TypeGuard``. + ``TypeGuard`` also works with type variables. See :pep:`647` for more details. + For example:: def is_str_list(val: list[object]) -> TypeGuard[list[str]]: @@ -1513,8 +1515,6 @@ These can be used as types in annotations. They all support subscription using the variable at all. When a ``TypeIs`` function returns ``False``, type checkers can narrow the type of the variable to exclude the ``TypeIs`` type. - ``TypeGuard`` also works with type variables. See :pep:`647` for more details. - .. versionadded:: 3.10