Skip to content

Adds better type narrowing docs #11088

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 4 commits into from
Sep 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 0 additions & 49 deletions docs/source/casts.rst

This file was deleted.

6 changes: 4 additions & 2 deletions docs/source/common_issues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ Complex type tests
Mypy can usually infer the types correctly when using :py:func:`isinstance <isinstance>`,
:py:func:`issubclass <issubclass>`,
or ``type(obj) is some_class`` type tests,
and even user-defined type guards,
and even :ref:`user-defined type guards <type-guards>`,
but for other kinds of checks you may need to add an
explicit type cast:

Expand All @@ -385,7 +385,7 @@ explicit type cast:

found = a[index] # Has `object` type, despite the fact that we know it is `str`
return cast(str, found) # So, we need an explicit cast to make mypy happy

Alternatively, you can use ``assert`` statement together with some
of the supported type inference techniques:

Expand Down Expand Up @@ -729,6 +729,8 @@ not necessary:
def test(self, t: List[int]) -> Sequence[str]: # type: ignore[override]
...

.. _unreachable:

Unreachable code
----------------

Expand Down
6 changes: 3 additions & 3 deletions docs/source/dynamic_typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ operations:
n = 1 # type: int
n = o # Error!

You can use :py:func:`~typing.cast` (see chapter :ref:`casts`) or :py:func:`isinstance` to
go from a general type such as :py:class:`object` to a more specific
type (subtype) such as ``int``. :py:func:`~typing.cast` is not needed with
You can use different :ref:`type narrowing <type-narrowing>`
techniques to narrow :py:class:`object` to a more specific
type (subtype) such as ``int``. Type narrowing is not needed with
dynamically typed values (values with type ``Any``).
2 changes: 1 addition & 1 deletion docs/source/generics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ non-function (e.g. ``my_decorator(1)``) will be rejected.
Also note that the ``wrapper()`` function is not type-checked. Wrapper
functions are typically small enough that this is not a big
problem. This is also the reason for the :py:func:`~typing.cast` call in the
``return`` statement in ``my_decorator()``. See :ref:`casts`.
``return`` statement in ``my_decorator()``. See :ref:`casts <casts>`.

.. _decorator-factories:

Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Mypy is a static type checker for Python 3 and Python 2.7.
protocols
dynamic_typing
python2
casts
type_narrowing
duck_type_compatibility
stubs
generics
Expand Down
145 changes: 0 additions & 145 deletions docs/source/more_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1151,148 +1151,3 @@ 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)
2 changes: 1 addition & 1 deletion docs/source/runtime_troubles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ required to be valid Python syntax. For more details, see :pep:`563`.
of forward references or generics in:

* :ref:`type aliases <type-aliases>`;
* :ref:`casts <casts>`;
* :ref:`type narrowing <type-narrowing>`;
* type definitions (see :py:class:`~typing.TypeVar`, :py:func:`~typing.NewType`, :py:class:`~typing.NamedTuple`);
* base classes.

Expand Down
Loading