Skip to content

Adds docs for TypeGuard type, #10591 #10758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Aug 3, 2021
145 changes: 145 additions & 0 deletions docs/source/more_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <literal_types>`. 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 <https://www.python.org/dev/peps/pep-0647/#enforcing-strict-narrowing>`_.
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 <https://www.python.org/dev/peps/pep-0647/#narrowing-of-implicit-self-and-cls-parameters>`_
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)