bpo-43766: Implement PEP 647 (User-Defined Type Guards) in typing.py (#25282)

This commit is contained in:
Ken Jin 2021-04-27 22:31:04 +08:00 committed by GitHub
parent d92513390a
commit 05ab4b60ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 0 deletions

View 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
""""""""""""""""""""""

View 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
======================

View file

@ -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__."""

View file

@ -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."""

View file

@ -0,0 +1,2 @@
Implement :pep:`647` in the :mod:`typing` module by adding
:data:`TypeGuard`.