zulip/docs/testing/mypy.md

163 lines
5.9 KiB
Markdown
Raw Normal View History

2016-06-26 18:49:35 +02:00
# Python static type checker (mypy)
[mypy](http://mypy-lang.org/) is a compile-time static type checker
for Python, allowing optional, gradual typing of Python code. Zulip
was fully annotated with mypy's Python 2 syntax in 2016, before our
migration to Python 3 in late 2017.
As a result, Zulip is in the process of migrating from using mypy's
Python 2 compatible syntax for type annotations (in which type
annotations are written inside comments that start with `# type: `) to
the Python 3 syntax. Here's a brief example of the mypy syntax we're
using in Zulip:
```
user_dict = {} # type: Dict[str, UserProfile]
2017-10-27 10:48:19 +02:00
def get_user(email: str, realm: Realm) -> UserProfile:
... # Actual code of the function here
```
You can learn more about it at:
* The
[mypy cheat sheet for Python 3](http://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html)
is the best resource for quickly understanding how to write the PEP
484 type annotations used by mypy correctly.
* The
[Python type annotation spec in PEP 484](https://www.python.org/dev/peps/pep-0484/)
* Our [blog post on being an early adopter of mypy][mypy-blog-post] from 2016.
The mypy type checker is run automatically as part of Zulip's Travis
CI testing process in the `backend` build.
[mypy-blog-post]: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/
## Installing mypy
mypy is installed by default in the Zulip development environment. If
you'd like to install just the version of `mypy` that we're using
(useful if e.g. you want `mypy` installed on your laptop outside the
Vagrant guest), you can do that with `pip install -r
requirements/mypy.txt`.
## Running mypy on Zulip's code locally
To run mypy on Zulip's python code, you can run the command:
tools/run-mypy
This will take a while to start running, since it runs mypy as a
long-running daemon (server) process and send type-checking requests
to the server; this makes checking mypy about 100x faster. But if
you're debugging or for whatever reason don't want the daemon, you can
use:
tools/run-mypy --no-daemon
Mypy outputs errors in the same style as a compiler would. For
example, if your code has a type error like this:
```
foo = 1
foo = '1'
```
you'll get an error like this:
```
test.py: note: In function "test":
test.py:200: error: Incompatible types in assignment (expression has type "str", variable has type "int")
```
## mypy stubs for third-party modules.
For the Python standard library and some popular third-party modules,
the [typeshed project](https://github.com/python/typeshed) has
[stubs](https://github.com/python/mypy/wiki/Creating-Stubs-For-Python-Modules),
basically the equivalent of C header files defining the types used in
these Python APIs.
For other third-party modules that we call from Zulip, one either
needs to add an `ignore_missing_imports` entry in `mypy.ini` in the
root of the project, letting `mypy` know that it's third-party code,
or add type stubs to the `stubs/` directory, which has type stubs that
mypy can use to type-check calls into that third-party module.
It's easy to add new stubs! Just read the docs, look at some of
existing examples to see how they work, and remember to remove the
`ignore_missing_imports` entry in `mypy.ini` when you add them.
For any third-party modules that don't have stubs, `mypy` treats
everything in the third-party module as an `Any`, which is the right
model (one certainly wouldn't want to need stubs for everything just
to use `mypy`!), but means the code can't be fully type-checked.
## `type_debug.py`
`zerver/lib/type_debug.py` has a useful decorator `print_types`. It
prints the types of the parameters of the decorated function and the
return type whenever that function is called. This can help find out
what parameter types a function is supposed to accept, or if
parameters with the wrong types are being passed to a function.
Here is an example using the interactive console:
```
>>> from zerver.lib.type_debug import print_types
>>>
>>> @print_types
... def func(x, y):
... return x + y
...
>>> func(1.0, 2)
func(float, int) -> float
3.0
>>> func('a', 'b')
func(str, str) -> str
'ab'
>>> func((1, 2), (3,))
func((int, int), (int,)) -> (int, int, int)
(1, 2, 3)
>>> func([1, 2, 3], [4, 5, 6, 7])
func([int, ...], [int, ...]) -> [int, ...]
[1, 2, 3, 4, 5, 6, 7]
```
`print_all` prints the type of the first item of lists. So `[int, ...]` represents
a list whose first element's type is `int`. Types of all items are not printed
because a list can have many elements, which would make the output too large.
Similarly in dicts, one key's type and the corresponding value's type are printed.
So `{1: 'a', 2: 'b', 3: 'c'}` will be printed as `{int: str, ...}`.
## Mypy is there to find bugs in Zulip before they impact users
For the purposes of Zulip development, you can treat `mypy` like a
much more powerful linter that can catch a wide range of bugs. If,
after running `tools/run-mypy` on your Zulip branch, you get mypy
errors, it's important to get to the bottom of the issue, not just do
something quick to silence the warnings. Possible explanations include:
* A bug in any new type annotations you added.
* A bug in the existing type annotations.
* A bug in Zulip!
* Some Zulip code is correct but confusingly reuses variables with
different types.
* A bug in mypy (though this is increasingly rare as mypy is now
fairly mature as a project).
Each explanation has its own solution, but in every case the result
should be solving the mypy warning in a way that makes the Zulip
codebase better. If you need help understanding an issue, please feel
free to mention @sharmaeklavya2 or @timabbott on the relevant pull
request or issue on GitHub.
If you think you have found a bug in Zulip or mypy, inform the zulip
developers by opening an issue on [Zulip's GitHub
repository](https://github.com/zulip/zulip/issues) or posting on
[zulip-devel](https://groups.google.com/d/forum/zulip-devel). If it's
indeed a mypy bug, we can help with reporting it upstream.