Skip to content

Commit

Permalink
Improve some dynamic typing docs (#14576)
Browse files Browse the repository at this point in the history
Linking #13681
  • Loading branch information
hauntsaninja authored Feb 2, 2023
1 parent 154fee1 commit ca2694f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 22 deletions.
80 changes: 65 additions & 15 deletions docs/source/dynamic_typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,39 @@
Dynamically typed code
======================

As mentioned earlier, bodies of functions that don't have any explicit
types in their function annotation are dynamically typed (operations
are checked at runtime). Code outside functions is statically typed by
default, and types of variables are inferred. This does usually the
right thing, but you can also make any variable dynamically typed by
defining it explicitly with the type ``Any``:
In :ref:`getting-started-dynamic-vs-static`, we discussed how bodies of functions
that don't have any explicit type annotations in their function are "dynamically typed"
and that mypy will not check them. In this section, we'll talk a little bit more
about what that means and how you can enable dynamic typing on a more fine grained basis.

In cases where your code is too magical for mypy to understand, you can make a
variable or parameter dynamically typed by explicitly giving it the type
``Any``. Mypy will let you do basically anything with a value of type ``Any``,
including assigning a value of type ``Any`` to a variable of any type (or vice
versa).

.. code-block:: python
from typing import Any
s = 1 # Statically typed (type int)
d: Any = 1 # Dynamically typed (type Any)
s = 'x' # Type check error
d = 'x' # OK
num = 1 # Statically typed (inferred to be int)
num = 'x' # error: Incompatible types in assignment (expression has type "str", variable has type "int")
dyn: Any = 1 # Dynamically typed (type Any)
dyn = 'x' # OK
num = dyn # No error, mypy will let you assign a value of type Any to any variable
num += 1 # Oops, mypy still thinks num is an int
You can think of ``Any`` as a way to locally disable type checking.
See :ref:`silencing-type-errors` for other ways you can shut up
the type checker.

Operations on Any values
------------------------

You can do anything using a value with type ``Any``, and type checker
does not complain:
You can do anything using a value with type ``Any``, and the type checker
will not complain:

.. code-block:: python
Expand All @@ -37,20 +49,53 @@ does not complain:
open(x).read()
return x
Values derived from an ``Any`` value also often have the type ``Any``
Values derived from an ``Any`` value also usually have the type ``Any``
implicitly, as mypy can't infer a more precise result type. For
example, if you get the attribute of an ``Any`` value or call a
``Any`` value the result is ``Any``:

.. code-block:: python
def f(x: Any) -> None:
y = x.foo() # y has type Any
y.bar() # Okay as well!
y = x.foo()
reveal_type(y) # Revealed type is "Any"
z = y.bar("mypy will let you do anything to y")
reveal_type(z) # Revealed type is "Any"
``Any`` types may propagate through your program, making type checking
less effective, unless you are careful.

Function parameters without annotations are also implicitly ``Any``:

.. code-block:: python
def f(x) -> None:
reveal_type(x) # Revealed type is "Any"
x.can.do["anything", x]("wants", 2)
You can make mypy warn you about untyped function parameters using the
:option:`--disallow-untyped-defs <mypy --disallow-untyped-defs>` flag.

Generic types missing type parameters will have those parameters implicitly
treated as ``Any``:

.. code-block:: python
from typing import List
def f(x: List) -> None:
reveal_type(x) # Revealed type is "builtins.list[Any]"
reveal_type(x[0]) # Revealed type is "Any"
x[0].anything_goes() # OK
You can make mypy warn you about untyped function parameters using the
:option:`--disallow-any-generics <mypy --disallow-any-generics>` flag.

Finally, another major source of ``Any`` types leaking into your program is from
third party libraries that mypy does not know about. This is particularly the case
when using the :option:`--ignore-missing-imports <mypy --ignore-missing-imports>`
flag. See :ref:`fix-missing-imports` for more information about this.

Any vs. object
--------------

Expand Down Expand Up @@ -80,6 +125,11 @@ operations:
n: int = 1
n = o # Error!
If you're not sure whether you need to use :py:class:`object` or ``Any``, use
:py:class:`object` -- only switch to using ``Any`` if you get a type checker
complaint.

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
Expand Down
2 changes: 2 additions & 0 deletions docs/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ easy to adopt mypy incrementally.
In order to get useful diagnostics from mypy, you must add *type annotations*
to your code. See the section below for details.

.. _getting-started-dynamic-vs-static:

Dynamic vs static typing
************************

Expand Down
17 changes: 12 additions & 5 deletions docs/source/running_mypy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ attribute of the module will automatically succeed:
# But this type checks, and x will have type 'Any'
x = does_not_exist.foobar()
This can result in mypy failing to warn you about errors in your code. Since
operations on ``Any`` result in ``Any``, these dynamic types can propagate
through your code, making type checking less effective. See
:ref:`dynamic-typing` for more information.

The next sections describe what each of these errors means and recommended next steps; scroll to
the section that matches your error.

Expand All @@ -245,7 +250,7 @@ unless they either have declared themselves to be
themselves on `typeshed <https://github.com/python/typeshed>`_, the repository
of types for the standard library and some 3rd party libraries.

If you are getting this error, try:
If you are getting this error, try to obtain type hints for the library you're using:

1. Upgrading the version of the library you're using, in case a newer version
has started to include type hints.
Expand All @@ -264,7 +269,7 @@ If you are getting this error, try:
adding the location to the ``MYPYPATH`` environment variable.

These stub files do not need to be complete! A good strategy is to use
stubgen, a program that comes bundled with mypy, to generate a first
:ref:`stubgen <stubgen>`, a program that comes bundled with mypy, to generate a first
rough draft of the stubs. You can then iterate on just the parts of the
library you need.

Expand All @@ -273,9 +278,11 @@ If you are getting this error, try:
:ref:`PEP 561 compliant packages <installed-packages>`.

If you are unable to find any existing type hints nor have time to write your
own, you can instead *suppress* the errors. All this will do is make mypy stop
reporting an error on the line containing the import: the imported module
will continue to be of type ``Any``.
own, you can instead *suppress* the errors.

All this will do is make mypy stop reporting an error on the line containing the
import: the imported module will continue to be of type ``Any``, and mypy may
not catch errors in its use.

1. To suppress a *single* missing import error, add a ``# type: ignore`` at the end of the
line containing the import.
Expand Down
42 changes: 40 additions & 2 deletions docs/source/type_inference_and_annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ Working around the issue is easy by adding a type annotation:
a: list[int] = [] # OK
foo(a)
.. _silencing-type-errors:

Silencing type errors
*********************

Expand Down Expand Up @@ -228,6 +230,8 @@ short explanation of the bug. To do that, use this format:
# Starting app on http://localhost:8000
app.run(8000) # type: ignore # `run()` in v2.0 accepts an `int`, as a port
Type ignore error codes
-----------------------

By default, mypy displays an error code for each error:

Expand All @@ -240,7 +244,21 @@ It is possible to add a specific error-code in your ignore comment (e.g.
``# type: ignore[attr-defined]``) to clarify what's being silenced. You can
find more information about error codes :ref:`here <silence-error-codes>`.

Similarly, you can also ignore all mypy errors in a file, by adding a
Other ways to silence errors
----------------------------

You can get mypy to silence errors about a specific variable by dynamically
typing it with ``Any``. See :ref:`dynamic-typing` for more information.

.. code-block:: python
from typing import Any
def f(x: Any, y: str) -> None:
x = 'hello'
x += 1 # OK
You can ignore all mypy errors in a file by adding a
``# mypy: ignore-errors`` at the top of the file:

.. code-block:: python
Expand All @@ -250,8 +268,28 @@ Similarly, you can also ignore all mypy errors in a file, by adding a
import unittest
...
You can also specify per-module configuration options in your :ref:`config-file`.
For example:

.. code-block:: ini
# Don't report errors in the 'package_to_fix_later' package
[mypy-package_to_fix_later.*]
ignore_errors = True
# Disable specific error codes in the 'tests' package
# Also don't require type annotations
[mypy-tests.*]
disable_error_code = var-annotated, has-type
allow_untyped_defs = True
# Silence import errors from the 'library_missing_types' package
[mypy-library_missing_types.*]
ignore_missing_imports = True
Finally, adding a ``@typing.no_type_check`` decorator to a class, method or
function has the effect of ignoring that class, method or function.
function causes mypy to avoid type checking that class, method or function
and to treat it as not having any type annotations.

.. code-block:: python
Expand Down

0 comments on commit ca2694f

Please sign in to comment.