diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 3a962553e68a..6f6239717942 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -1132,3 +1132,148 @@ section of the docs has a full description with an example, but in short, you wi need to give each TypedDict the same key where each value has a unique unique :ref:`Literal type `. Then, check that key to distinguish between your TypedDicts. + + +User-Defined Type Guards +************************ + +Mypy supports User-Defined Type Guards +(:pep:`647`). + +A type guard is a way for programs to influence conditional +type narrowing employed by a type checker based on runtime checks. + +Basically, a ``TypeGuard`` is a "smart" alias for a ``bool`` type. +Let's have a look at the regular ``bool`` example: + +.. code-block:: python + + from typing import List + + def is_str_list(val: List[object]) -> bool: + """Determines whether all objects in the list are strings""" + return all(isinstance(x, str) for x in val) + + def func1(val: List[object]) -> None: + if is_str_list(val): + reveal_type(val) # Reveals List[object] + print(" ".join(val)) # Error: incompatible type + +The same example with ``TypeGuard``: + +.. code-block:: python + + from typing import List + from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below + + 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]) -> None: + if is_str_list(val): + reveal_type(val) # List[str] + print(" ".join(val)) # ok + +How does it work? ``TypeGuard`` narrows the first function argument (``val``) +to the type specified as the first type parameter (``List[str]``). + +.. note:: + + Narrowing is + `not strict `_. + For example, you can narrow ``str`` to ``int``: + + .. code-block:: python + + def f(value: str) -> TypeGuard[int]: + return True + + Note: since strict narrowing is not enforced, it's easy + to break type safety. + + However, there are many ways a determined or uninformed developer can + subvert type safety -- most commonly by using cast or Any. + If a Python developer takes the time to learn about and implement + user-defined type guards within their code, + it is safe to assume that they are interested in type safety + and will not write their type guard functions in a way + that will undermine type safety or produce nonsensical results. + +Generic TypeGuards +------------------ + +``TypeGuard`` can also work with generic types: + +.. code-block:: python + + from typing import Tuple, TypeVar + from typing import TypeGuard # use `typing_extensions` for `python<3.10` + + _T = TypeVar("_T") + + def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeGuard[Tuple[_T, _T]]: + return len(val) == 2 + + def func(names: Tuple[str, ...]): + if is_two_element_tuple(names): + reveal_type(names) # Tuple[str, str] + else: + reveal_type(names) # Tuple[str, ...] + +Typeguards with parameters +-------------------------- + +Type guard functions can accept extra arguments: + +.. code-block:: python + + from typing import Type, Set, TypeVar + from typing import TypeGuard # use `typing_extensions` for `python<3.10` + + _T = TypeVar("_T") + + def is_set_of(val: Set[Any], type: Type[_T]) -> TypeGuard[Set[_T]]: + return all(isinstance(x, type) for x in val) + + items: Set[Any] + if is_set_of(items, str): + reveal_type(items) # Set[str] + +TypeGuards as methods +--------------------- + + A method can also serve as the ``TypeGuard``: + +.. code-block:: python + + class StrValidator: + def is_valid(self, instance: object) -> TypeGuard[str]: + return isinstance(instance, str) + + def func(to_validate: object) -> None: + if StrValidator().is_valid(to_validate): + reveal_type(to_validate) # Revealed type is "builtins.str" + +.. note:: + + Note, that ``TypeGuard`` + `does not narrow `_ + types of ``self`` or ``cls`` implicit arguments. + + If narrowing of ``self`` or ``cls`` is required, + the value can be passed as an explicit argument to a type guard function: + + .. code-block:: python + + class Parent: + def method(self) -> None: + reveal_type(self) # Revealed type is "Parent" + if is_child(self): + reveal_type(self) # Revealed type is "Child" + + class Child(Parent): + ... + + def is_child(instance: Parent) -> TypeGuard[Child]: + return isinstance(instance, Child)