mirror of https://github.com/zulip/zulip.git
201 lines
7.5 KiB
Markdown
201 lines
7.5 KiB
Markdown
# 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. In 2018 and 2020, we migrated
|
|
essentially the entire codebase to the nice PEP 484 (Python 3 only)
|
|
and PEP 526 (Python 3.6) syntax for static types:
|
|
|
|
```
|
|
user_dict: Dict[str, UserProfile] = {}
|
|
|
|
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](https://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 2 cheat sheet](https://mypy.readthedocs.io/en/latest/cheat_sheet.html)
|
|
is useful for understanding the type comment syntax needed for our
|
|
few modules that need to support both Python 2 and 3.
|
|
|
|
* 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
|
|
|
|
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 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, before we merge the changes.
|
|
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're having trouble, silence the warning with
|
|
an `Any` or `# type: ignore[code]` so you're not blocked waiting for help,
|
|
add a `# TODO: ` comment so it doesn't get forgotten in code review,
|
|
and ask for help in chat.zulip.org.
|
|
|
|
## 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, ...}`.
|
|
|
|
## Using @overload to accurately describe variations
|
|
|
|
Sometimes, a function's type is most precisely expressed as a few
|
|
possibilities, and which possibility can be determined by looking at
|
|
the arguments. You can express that idea in a way mypy understands
|
|
using `@overload`. For example, `check_list` returns a `Validator`
|
|
function that verifies that an object is a list, raising an exception
|
|
if it isn't.
|
|
|
|
It supports being passed a `sub_validator`, which will verify that
|
|
each element in the list has a given type as well. One can express
|
|
the idea "If `sub_validator` validates that something is a `ResultT`,
|
|
`check_list(sub_validator)` validators that something is a
|
|
`List[ResultT]` as follows:
|
|
|
|
~~~ py
|
|
@overload
|
|
def check_list(sub_validator: None, length: Optional[int]=None) -> Validator[List[object]]:
|
|
...
|
|
@overload
|
|
def check_list(sub_validator: Validator[ResultT],
|
|
length: Optional[int]=None) -> Validator[List[ResultT]]:
|
|
...
|
|
def check_list(sub_validator: Optional[Validator[ResultT]]=None,
|
|
length: Optional[int]=None) -> Validator[List[ResultT]]:
|
|
~~~
|
|
|
|
The first overload expresses the types for the case where no
|
|
`sub_validator` is passed, in which case all we know is that it
|
|
returns a `Validator[List[object]]`; whereas the second defines the
|
|
type logic for the case where we are passed a `sub_validator`.
|
|
|
|
See the [mypy overloading documentation][mypy-overloads] for more details.
|
|
|
|
[mypy-overloads]: https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading
|
|
|
|
## Troubleshooting advice
|
|
|
|
All of our linters, including mypy, are designed to only check files
|
|
that have been added in git (this is by design, since it means you
|
|
have untracked files in your Zulip checkout safely). So if you get a
|
|
`mypy` error like this after adding a new file that is referenced by
|
|
the existing codebase:
|
|
|
|
```
|
|
mypy | zerver/models.py:1234: note: Import of 'zerver.lib.markdown_wrappers' ignored
|
|
mypy | zerver/models.py:1234: note: (Using --follow-imports=error, module not passed on command line)
|
|
```
|
|
|
|
The problem is that you need to `git add` the new file.
|