Skip to content

Commit

Permalink
Adds better type narrowing docs (#11088)
Browse files Browse the repository at this point in the history
Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com>
  • Loading branch information
sobolevn and hauntsaninja committed Sep 19, 2021
1 parent b165e42 commit b3ff2a6
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 202 deletions.
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
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
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
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
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
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
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

0 comments on commit b3ff2a6

Please sign in to comment.