2016-06-26 18:49:35 +02:00
|
|
|
|
# Python static type checker (mypy)
|
2016-04-27 10:28:12 +02:00
|
|
|
|
|
|
|
|
|
[mypy](http://mypy-lang.org/) is a compile-time static type checker
|
2021-08-20 21:53:28 +02:00
|
|
|
|
for Python, allowing optional, gradual typing of Python code. Zulip
|
2018-01-23 20:14:35 +01:00
|
|
|
|
was fully annotated with mypy's Python 2 syntax in 2016, before our
|
2021-08-20 21:53:28 +02:00
|
|
|
|
migration to Python 3 in late 2017. In 2018 and 2020, we migrated
|
2020-05-09 00:10:17 +02:00
|
|
|
|
essentially the entire codebase to the nice PEP 484 (Python 3 only)
|
|
|
|
|
and PEP 526 (Python 3.6) syntax for static types:
|
2016-04-27 10:28:12 +02:00
|
|
|
|
|
2021-03-12 03:41:18 +01:00
|
|
|
|
```python
|
2020-05-09 00:10:17 +02:00
|
|
|
|
user_dict: Dict[str, UserProfile] = {}
|
2016-04-27 10:28:12 +02:00
|
|
|
|
|
2017-10-27 10:48:19 +02:00
|
|
|
|
def get_user(email: str, realm: Realm) -> UserProfile:
|
2016-04-27 10:28:12 +02:00
|
|
|
|
... # Actual code of the function here
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You can learn more about it at:
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- The
|
2020-03-27 01:32:21 +01:00
|
|
|
|
[mypy cheat sheet for Python 3](https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html)
|
2018-05-14 02:23:01 +02:00
|
|
|
|
is the best resource for quickly understanding how to write the PEP
|
2022-10-06 05:19:17 +02:00
|
|
|
|
484 type annotations used by mypy correctly.
|
2016-06-10 12:58:19 +02:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- The
|
2021-03-12 03:41:18 +01:00
|
|
|
|
[Python type annotation spec in PEP 484](https://www.python.org/dev/peps/pep-0484/).
|
2016-04-27 10:28:12 +02:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- Our [blog post on being an early adopter of mypy][mypy-blog-post] from 2016.
|
2018-12-17 05:58:06 +01:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- Our [best practices](#best-practices) section below.
|
2021-03-12 03:41:18 +01:00
|
|
|
|
|
2016-04-27 10:28:12 +02:00
|
|
|
|
The mypy type checker is run automatically as part of Zulip's Travis
|
2017-08-27 22:39:58 +02:00
|
|
|
|
CI testing process in the `backend` build.
|
2016-04-27 10:28:12 +02:00
|
|
|
|
|
2018-12-17 05:58:06 +01:00
|
|
|
|
[mypy-blog-post]: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/
|
2018-07-27 00:06:22 +02:00
|
|
|
|
|
2018-12-17 06:10:22 +01:00
|
|
|
|
## Installing mypy
|
|
|
|
|
|
2022-09-08 02:44:58 +02:00
|
|
|
|
mypy is installed by default in the Zulip development environment.
|
2018-12-17 06:10:22 +01:00
|
|
|
|
|
|
|
|
|
## Running mypy on Zulip's code locally
|
|
|
|
|
|
|
|
|
|
To run mypy on Zulip's python code, you can run the command:
|
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
|
```bash
|
|
|
|
|
tools/run-mypy
|
|
|
|
|
```
|
2018-12-17 06:10:22 +01:00
|
|
|
|
|
2021-08-20 21:53:28 +02:00
|
|
|
|
Mypy outputs errors in the same style as a compiler would. For
|
2018-12-17 06:10:22 +01:00
|
|
|
|
example, if your code has a type error like this:
|
|
|
|
|
|
2021-03-12 03:41:18 +01:00
|
|
|
|
```python
|
2018-12-17 06:10:22 +01:00
|
|
|
|
foo = 1
|
|
|
|
|
foo = '1'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
you'll get an error like this:
|
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
|
```console
|
2018-12-17 06:10:22 +01:00
|
|
|
|
test.py: note: In function "test":
|
|
|
|
|
test.py:200: error: Incompatible types in assignment (expression has type "str", variable has type "int")
|
|
|
|
|
```
|
|
|
|
|
|
2018-12-17 19:30:51 +01:00
|
|
|
|
## Mypy is there to find bugs in Zulip before they impact users
|
|
|
|
|
|
|
|
|
|
For the purposes of Zulip development, you can treat `mypy` like a
|
2021-08-20 21:53:28 +02:00
|
|
|
|
much more powerful linter that can catch a wide range of bugs. If,
|
2018-12-17 19:30:51 +01:00
|
|
|
|
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:
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- 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
|
2018-12-17 19:30:51 +01:00
|
|
|
|
different types.
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- A bug in mypy (though this is increasingly rare as mypy is now
|
2018-12-17 19:30:51 +01:00
|
|
|
|
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
|
2021-08-20 21:53:28 +02:00
|
|
|
|
codebase better. If you're having trouble, silence the warning with
|
2020-04-22 04:13:37 +02:00
|
|
|
|
an `Any` or `# type: ignore[code]` so you're not blocked waiting for help,
|
2018-12-17 19:30:51 +01:00
|
|
|
|
add a `# TODO: ` comment so it doesn't get forgotten in code review,
|
|
|
|
|
and ask for help in chat.zulip.org.
|
|
|
|
|
|
2020-08-11 01:47:54 +02:00
|
|
|
|
## Mypy stubs for third-party modules
|
2018-07-27 00:06:22 +02:00
|
|
|
|
|
|
|
|
|
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
|
2021-07-05 23:36:46 +02:00
|
|
|
|
needs to add an `ignore_missing_imports` entry in `pyproject.toml` in the
|
2018-07-27 00:06:22 +02:00
|
|
|
|
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.
|
|
|
|
|
|
2021-08-20 21:53:28 +02:00
|
|
|
|
It's easy to add new stubs! Just read the docs, look at some of
|
2018-07-27 00:06:22 +02:00
|
|
|
|
existing examples to see how they work, and remember to remove the
|
2021-07-05 23:36:46 +02:00
|
|
|
|
`ignore_missing_imports` entry in `pyproject.toml` when you add them.
|
2018-07-27 00:06:22 +02:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2022-10-06 06:12:56 +02:00
|
|
|
|
## Working with types from django-stubs
|
|
|
|
|
|
|
|
|
|
For features that are difficult to be expressed with static type
|
|
|
|
|
annotations, type analysis is supplemented with mypy plugins. Zulip's
|
|
|
|
|
Python codebases uses the Django web framework, and such a plugin is
|
|
|
|
|
required in order for `mypy` to correctly infer the types of most code
|
|
|
|
|
interacting with Django model classes (i.e. code that accesses the
|
|
|
|
|
database).
|
|
|
|
|
|
|
|
|
|
We use the `mypy_django_plugin` plugin from the
|
|
|
|
|
[django-stubs](https://github.com/typeddjango/django-stubs) project,
|
|
|
|
|
which supports accurate type inference for classes like
|
|
|
|
|
`QuerySet`. For example, `Stream.objects.filter(realm=realm)` is
|
|
|
|
|
simple Django code to fetch all the streams in a realm. With this
|
|
|
|
|
plugin, mypy will correctly determine its type is `QuerySet[Stream]`,
|
|
|
|
|
aka a standard, lazily evaluated Django query object that can be
|
|
|
|
|
iterated through to access `Stream` objects, without the developer
|
|
|
|
|
needing to do an explicit annotation.
|
|
|
|
|
|
|
|
|
|
When declaring the types for functions that accept a `QuerySet`
|
|
|
|
|
object, you should always supply the model type that it accepts as the
|
|
|
|
|
type parameter.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def foo(user: QuerySet[UserProfile]) -> None:
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
In cases where you need to type the return value from `.values_list`
|
|
|
|
|
or `.values` on a `QuerySet`, you can use the special
|
|
|
|
|
`django_stubs_ext.ValuesQuerySet` type.
|
|
|
|
|
|
|
|
|
|
For `.values_list`, the second type parameter will be the type of the
|
|
|
|
|
column.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from django_stubs_ext import ValuesQuerySet
|
|
|
|
|
|
|
|
|
|
def get_book_page_counts() -> ValuesQuerySet[Book, int]:
|
|
|
|
|
return Book.objects.filter().values_list("page_count", flat=True)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For `.values`, we prefer to define a `TypedDict` containing the
|
|
|
|
|
key-value pairs for the columns.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from django_stubs_ext import ValuesQuerySet
|
|
|
|
|
|
|
|
|
|
class BookMetadata(TypedDict):
|
|
|
|
|
id: int
|
|
|
|
|
name: str
|
|
|
|
|
|
|
|
|
|
def get_book_meta_data(
|
|
|
|
|
book_ids: List[int],
|
|
|
|
|
) -> ValuesQuerySet[Book, BookMetadata]:
|
|
|
|
|
return Book.objects.filter(id__in=book_ids).values("name", "id")
|
|
|
|
|
```
|
|
|
|
|
|
2022-10-07 22:47:38 +02:00
|
|
|
|
When writing a helper function that returns the response from a test
|
2022-10-06 06:12:56 +02:00
|
|
|
|
client, it should be typed as `TestHttpResponse` instead of
|
|
|
|
|
`HttpResponse`. This type is only defined in the Django stubs, so it
|
|
|
|
|
has to be conditionally imported only when type
|
|
|
|
|
checking. Conventionally, we alias it as `TestHttpResponse`, which is
|
|
|
|
|
internally named `_MonkeyPatchedWSGIResponse` within django-stubs.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
|
|
|
|
|
|
|
|
|
|
class FooTestCase(ZulipTestCase):
|
|
|
|
|
def helper(self) -> "TestHttpResponse":
|
|
|
|
|
return self.client_get("/bar")
|
|
|
|
|
```
|
|
|
|
|
|
2023-10-09 20:41:12 +02:00
|
|
|
|
We sometimes encounter inaccurate type annotations in the Django
|
2023-01-02 20:50:23 +01:00
|
|
|
|
stubs project. We prefer to address these by [submitting a pull
|
2022-10-06 06:12:56 +02:00
|
|
|
|
request](https://github.com/typeddjango/django-stubs/pulls) to fix the
|
|
|
|
|
issue in the upstream project, just like we do with `typeshed` bugs.
|
|
|
|
|
|
2020-06-21 19:51:35 +02:00
|
|
|
|
## Using @overload to accurately describe variations
|
|
|
|
|
|
|
|
|
|
Sometimes, a function's type is most precisely expressed as a few
|
2020-08-11 01:47:44 +02:00
|
|
|
|
possibilities, and which possibility can be determined by looking at
|
2021-08-20 21:53:28 +02:00
|
|
|
|
the arguments. You can express that idea in a way mypy understands
|
|
|
|
|
using `@overload`. For example, `check_list` returns a `Validator`
|
2020-06-21 19:51:35 +02:00
|
|
|
|
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
|
2021-08-20 21:53:28 +02:00
|
|
|
|
each element in the list has a given type as well. One can express
|
2020-06-21 19:51:35 +02:00
|
|
|
|
the idea "If `sub_validator` validates that something is a `ResultT`,
|
2024-01-01 21:35:46 +01:00
|
|
|
|
`check_list(sub_validator)` validates that something is a
|
2020-06-21 19:51:35 +02:00
|
|
|
|
`List[ResultT]` as follows:
|
|
|
|
|
|
2021-03-12 03:41:18 +01:00
|
|
|
|
```python
|
2020-06-21 19:51:35 +02:00
|
|
|
|
@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]]:
|
2021-03-12 03:41:18 +01:00
|
|
|
|
```
|
2020-06-21 19:51:35 +02:00
|
|
|
|
|
|
|
|
|
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`.
|
|
|
|
|
|
2021-03-12 03:41:18 +01:00
|
|
|
|
**Warning:** Mypy only checks the body of an overloaded function
|
|
|
|
|
against the final signature and not against the more restrictive
|
2021-08-20 21:53:28 +02:00
|
|
|
|
`@overload` signatures. This allows some type errors to evade
|
2021-03-12 03:41:18 +01:00
|
|
|
|
detection by mypy:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
@overload
|
|
|
|
|
def f(x: int) -> int: ...
|
|
|
|
|
@overload
|
|
|
|
|
def f(x: str) -> int: ... # oops
|
|
|
|
|
def f(x: Union[int, str]) -> Union[int, str]:
|
|
|
|
|
return x
|
|
|
|
|
|
|
|
|
|
x: int = f("three!!")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Due to this potential for unsafety, we discourage overloading unless
|
2021-08-20 21:53:28 +02:00
|
|
|
|
it's absolutely necessary. Consider writing multiple functions with
|
2021-03-12 03:41:18 +01:00
|
|
|
|
different names instead.
|
|
|
|
|
|
2020-06-21 19:51:35 +02:00
|
|
|
|
See the [mypy overloading documentation][mypy-overloads] for more details.
|
|
|
|
|
|
|
|
|
|
[mypy-overloads]: https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading
|
|
|
|
|
|
2021-03-12 03:41:18 +01:00
|
|
|
|
## Best practices
|
|
|
|
|
|
|
|
|
|
### When is a type annotation justified?
|
|
|
|
|
|
|
|
|
|
Usually in fully typed code, mypy will protect you from writing a type
|
2021-08-20 21:53:28 +02:00
|
|
|
|
annotation that isn't justified by the surrounding code. But when you
|
2021-03-12 03:41:18 +01:00
|
|
|
|
need to write annotations at the border between untyped and typed
|
|
|
|
|
code, keep in mind that **a type annotation should always represent a
|
2021-08-20 21:53:28 +02:00
|
|
|
|
guarantee,** not an aspiration. If you have validated that some value
|
|
|
|
|
is an `int`, it can go in an `int` annotated variable. If you are
|
|
|
|
|
going to validate it later, it should not. When in doubt, an `object`
|
2021-03-12 03:41:18 +01:00
|
|
|
|
annotation is always safe.
|
|
|
|
|
|
|
|
|
|
Mypy understands many Python constructs like `assert`, `if`,
|
|
|
|
|
`isinstance`, and logical operators, and uses them to automatically
|
|
|
|
|
narrow the type of validated objects in many cases.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def f(x: object, y: Optional[str]) -> None:
|
|
|
|
|
if isinstance(x, int):
|
|
|
|
|
# Within this if block, mypy infers that x: int
|
|
|
|
|
print(x + 1)
|
|
|
|
|
assert y is not None
|
|
|
|
|
# After that assert statement, mypy infers that y: str
|
|
|
|
|
print(y.strip())
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It won't be able do this narrowing if the validation is hidden behind
|
|
|
|
|
a function call, so sometimes it's helpful for a validation function
|
|
|
|
|
to return the type-narrowed value back to the caller even though the
|
2021-08-20 21:53:28 +02:00
|
|
|
|
caller already has it. (The validators in `zerver/lib/validator.py`
|
2021-03-12 03:41:18 +01:00
|
|
|
|
are examples of this pattern.)
|
|
|
|
|
|
|
|
|
|
### Avoid the `Any` type
|
|
|
|
|
|
|
|
|
|
Mypy provides the [`Any`
|
|
|
|
|
type](https://mypy.readthedocs.io/en/stable/dynamic_typing.html) for
|
|
|
|
|
interoperability with untyped code, but it is completely unchecked.
|
|
|
|
|
You can put an value of an arbitrary type into an expression of type
|
|
|
|
|
`Any`, and get an value of an arbitrary type out, and mypy will make
|
2021-08-20 21:53:28 +02:00
|
|
|
|
no effort to check that the input and output types match. So using
|
2021-03-12 03:41:18 +01:00
|
|
|
|
`Any` defeats the type safety that mypy would otherwise provide.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
x: Any = 5
|
|
|
|
|
y: str = x # oops
|
|
|
|
|
print(y.strip()) # runtime error
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If you think you need to use `Any`, consider the following safer
|
|
|
|
|
alternatives first:
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- To annotate a dictionary where different keys correspond to values
|
2021-03-12 03:41:18 +01:00
|
|
|
|
of different types, instead of writing `Dict[str, Any]`, try
|
|
|
|
|
declaring a
|
|
|
|
|
[**`dataclass`**](https://mypy.readthedocs.io/en/stable/additional_features.html#dataclasses)
|
|
|
|
|
or a
|
|
|
|
|
[**`TypedDict`**](https://mypy.readthedocs.io/en/stable/more_types.html#typeddict).
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- If you're annotating a class or function that might be used with
|
2021-03-12 03:41:18 +01:00
|
|
|
|
different data types at different call sites, similar to the builtin
|
|
|
|
|
`List` type or the `sorted` function, [**generic
|
|
|
|
|
types**](https://mypy.readthedocs.io/en/stable/generics.html) with
|
|
|
|
|
`TypeVar` might be what you need.
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- If you need to accept data of several specific possible types at a
|
2021-03-12 03:41:18 +01:00
|
|
|
|
single site, you may want a [**`Union`
|
|
|
|
|
type**](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#union-types).
|
|
|
|
|
`Union` is checked: before using `value: Union[str, int]` as a
|
2021-09-08 00:23:24 +02:00
|
|
|
|
`str`, mypy requires that you validate it with an
|
|
|
|
|
`instance(value, str)` test.
|
2021-03-12 03:41:18 +01:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- If you really have no information about the type of a value, use the
|
2021-08-20 21:53:28 +02:00
|
|
|
|
**`object` type**. Since every type is a subtype of `object`, you
|
|
|
|
|
can correctly annotate any value as `object`. The [difference
|
2021-03-12 03:41:18 +01:00
|
|
|
|
between `Any` and
|
|
|
|
|
`object`](https://mypy.readthedocs.io/en/stable/dynamic_typing.html#any-vs-object)
|
|
|
|
|
is that mypy will check that you safely validate an `object` with
|
|
|
|
|
`isinstance` before using it in a way that expects a more specific
|
|
|
|
|
type.
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- A common way for `Any` annotations to sneak into your code is the
|
2021-08-20 21:53:28 +02:00
|
|
|
|
interaction with untyped third-party libraries. Mypy treats any
|
2021-03-12 03:41:18 +01:00
|
|
|
|
value imported from an untyped library as annotated with `Any`, and
|
|
|
|
|
treats any type imported from an untyped library as equivalent to
|
2021-08-20 21:53:28 +02:00
|
|
|
|
`Any`. Consider providing real type annotations for the library by
|
2021-03-12 03:41:18 +01:00
|
|
|
|
[**writing a stub file**](#mypy-stubs-for-third-party-modules).
|
|
|
|
|
|
|
|
|
|
### Avoid `cast()`
|
|
|
|
|
|
|
|
|
|
The [`cast`
|
2022-02-17 21:41:36 +01:00
|
|
|
|
function](https://mypy.readthedocs.io/en/stable/type_narrowing.html#casts) lets you
|
2021-08-20 21:53:28 +02:00
|
|
|
|
provide an annotation that Mypy will not verify. Obviously, this is
|
2021-03-12 03:41:18 +01:00
|
|
|
|
completely unsafe in general.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
x = cast(str, 5) # oops
|
|
|
|
|
print(x.strip()) # runtime error
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Instead of using `cast`:
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- You can use a [variable
|
2021-03-12 03:41:18 +01:00
|
|
|
|
annotation](https://mypy.readthedocs.io/en/stable/type_inference_and_annotations.html#explicit-types-for-variables)
|
|
|
|
|
to be explicit or to disambiguate types that mypy can check but
|
|
|
|
|
cannot infer.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
l: List[int] = []
|
|
|
|
|
```
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- You can use an [`isinstance`
|
2021-03-12 03:41:18 +01:00
|
|
|
|
test](https://mypy.readthedocs.io/en/stable/common_issues.html#complex-type-tests)
|
|
|
|
|
to safely verify that a value really has the type you expect.
|
|
|
|
|
|
|
|
|
|
### Avoid `# type: ignore` comments
|
|
|
|
|
|
2021-09-08 00:23:24 +02:00
|
|
|
|
Mypy allows you to ignore any type checking error with a
|
|
|
|
|
[`# type: ignore`
|
2021-03-12 03:41:18 +01:00
|
|
|
|
comment](https://mypy.readthedocs.io/en/stable/common_issues.html#spurious-errors-and-locally-silencing-the-checker),
|
|
|
|
|
but you should avoid this in the absence of a very good reason, such
|
2021-08-20 21:53:28 +02:00
|
|
|
|
as a bug in mypy itself. If there are no safe options for dealing
|
2021-03-12 03:41:18 +01:00
|
|
|
|
with the error, prefer an unchecked `cast`, since its unsafety is
|
|
|
|
|
somewhat more localized.
|
|
|
|
|
|
|
|
|
|
Our linter requires all `# type: ignore` comments to be [scoped to the
|
|
|
|
|
specific error
|
|
|
|
|
code](https://mypy.readthedocs.io/en/stable/error_codes.html) being
|
|
|
|
|
ignored, and followed by an explanation such as a link to a GitHub
|
|
|
|
|
issue.
|
|
|
|
|
|
|
|
|
|
### Avoid other unchecked constructs
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- As mentioned
|
2021-03-12 03:41:18 +01:00
|
|
|
|
[above](#using-overload-to-accurately-describe-variations), we
|
|
|
|
|
**discourage writing overloaded functions** because their bodies are
|
|
|
|
|
not checked against the `@overload` signatures.
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- **Avoid `Callable[..., T]`** (with literal ellipsis `...`), since
|
2021-08-20 21:53:28 +02:00
|
|
|
|
mypy cannot check the types of arguments passed to it. Provide the
|
2021-03-12 03:41:18 +01:00
|
|
|
|
specific argument types (`Callable[[int, str], T]`) in simple cases,
|
|
|
|
|
or use [callback
|
|
|
|
|
protocols](https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols)
|
|
|
|
|
in more complex cases.
|
|
|
|
|
|
|
|
|
|
### Use `Optional` and `None` correctly
|
|
|
|
|
|
|
|
|
|
The [`Optional`
|
|
|
|
|
type](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html#built-in-types)
|
2021-08-20 21:53:28 +02:00
|
|
|
|
is for optional values, which are values that could be `None`. For
|
2021-03-12 03:41:18 +01:00
|
|
|
|
example, `Optional[int]` is equivalent to `Union[int, None]`.
|
|
|
|
|
|
|
|
|
|
The `Optional` type is **not for optional parameters** (unless they
|
2021-08-20 21:53:28 +02:00
|
|
|
|
are also optional values as above). This signature does not use the
|
2021-03-12 03:41:18 +01:00
|
|
|
|
`Optional` type:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def func(flag: bool = False) -> str:
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A collection such as `List` should only be `Optional` if `None` would
|
|
|
|
|
have a different meaning than the natural meaning of an empty
|
2021-08-20 21:53:28 +02:00
|
|
|
|
collection. For example:
|
2021-03-12 03:41:18 +01:00
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- An include list where the default is to include everything should be
|
2021-03-12 03:41:18 +01:00
|
|
|
|
`Optional` with default `None`.
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- An exclude list where the default is to exclude nothing should be
|
2021-03-12 03:41:18 +01:00
|
|
|
|
non-`Optional` with default `[]`.
|
|
|
|
|
|
2021-09-08 00:23:24 +02:00
|
|
|
|
Don't test an `Optional` value using truthiness (`if value:`,
|
|
|
|
|
`not value`, `value or default_value`), especially when the type might
|
|
|
|
|
have falsy values other than `None`.
|
2021-03-12 03:41:18 +01:00
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
s: Optional[str]
|
|
|
|
|
if not s: # bad: are we checking for None or ""?
|
|
|
|
|
...
|
|
|
|
|
if s is None: # good
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Read-only types
|
|
|
|
|
|
2021-04-29 23:32:39 +02:00
|
|
|
|
The basic Python collections
|
|
|
|
|
[`List`](https://docs.python.org/3/library/typing.html#typing.List),
|
|
|
|
|
[`Dict`](https://docs.python.org/3/library/typing.html#typing.Dict),
|
|
|
|
|
and [`Set`](https://docs.python.org/3/library/typing.html#typing.Set)
|
|
|
|
|
are mutable, but it's confusing for a function to mutate a collection
|
2021-08-20 21:53:28 +02:00
|
|
|
|
that was passed to it as an argument, especially by accident. To
|
2021-04-29 23:32:39 +02:00
|
|
|
|
avoid this, prefer annotating function parameters with read-only
|
|
|
|
|
types:
|
|
|
|
|
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)
|
2021-04-29 23:32:39 +02:00
|
|
|
|
instead of `List`,
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)
|
2021-04-29 23:32:39 +02:00
|
|
|
|
instead of `Dict`,
|
2021-08-20 21:45:39 +02:00
|
|
|
|
- [`AbstractSet`](https://docs.python.org/3/library/typing.html#typing.AbstractSet)
|
2021-04-29 23:32:39 +02:00
|
|
|
|
instead of `Set`.
|
2021-03-12 03:41:18 +01:00
|
|
|
|
|
|
|
|
|
This is especially important for parameters with default arguments,
|
|
|
|
|
since a mutable default argument is confusingly shared between all
|
|
|
|
|
calls to the function.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def f(items: Sequence[int] = []) -> int:
|
|
|
|
|
items.append(1) # mypy catches this mistake
|
|
|
|
|
return sum(items)
|
|
|
|
|
```
|
|
|
|
|
|
2021-04-29 23:32:39 +02:00
|
|
|
|
In some cases the more general
|
|
|
|
|
[`Collection`](https://docs.python.org/3/library/typing.html#typing.Collection)
|
|
|
|
|
or
|
|
|
|
|
[`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)
|
2021-08-20 21:53:28 +02:00
|
|
|
|
types might be appropriate. (But don’t use `Iterable` for a value
|
2021-04-29 23:32:39 +02:00
|
|
|
|
that might be iterated multiple times, since a one-use iterator is
|
|
|
|
|
`Iterable` too.)
|
|
|
|
|
|
2022-10-06 05:52:08 +02:00
|
|
|
|
For example, if a function gets called with either a `list` or a `QuerySet`,
|
|
|
|
|
and it only iterates the object once, the parameter can be typed as `Iterable`.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def f(items: Iterable[Realm]) -> None:
|
|
|
|
|
for item in items:
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
realms_list: List[Realm] = [zulip, analytics]
|
|
|
|
|
realms_queryset: QuerySet[Realm] = Realm.objects.all()
|
|
|
|
|
|
|
|
|
|
f(realms_list) # OK
|
|
|
|
|
f(realms_queryset) # Also OK
|
|
|
|
|
```
|
|
|
|
|
|
2021-03-12 03:41:18 +01:00
|
|
|
|
A function's return type can be mutable if the return value is always
|
|
|
|
|
a freshly created collection, since the caller ends up with the only
|
|
|
|
|
reference to the value and can freely mutate it without risk of
|
2021-08-20 21:53:28 +02:00
|
|
|
|
confusion. But a read-only return type might be more appropriate for
|
2021-03-12 03:41:18 +01:00
|
|
|
|
a function that returns a reference to an existing collection.
|
|
|
|
|
|
|
|
|
|
Read-only types have the additional advantage of being [covariant
|
|
|
|
|
rather than
|
|
|
|
|
invariant](https://mypy.readthedocs.io/en/latest/common_issues.html#invariance-vs-covariance):
|
|
|
|
|
if `B` is a subtype of `A`, then `List[B]` may not be converted to
|
|
|
|
|
`List[A]`, but `Sequence[B]` may be converted to `Sequence[A]`.
|
|
|
|
|
|
|
|
|
|
### Typing decorators
|
|
|
|
|
|
|
|
|
|
A simple decorator that operates on functions of a fixed signature
|
|
|
|
|
works with no issues:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def fancy(func: Callable[[str], str]) -> Callable[[int], str]:
|
|
|
|
|
def wrapped_func(n: int) -> str:
|
|
|
|
|
print("so fancy")
|
|
|
|
|
return func(str(n))
|
|
|
|
|
return wrapped_func
|
|
|
|
|
|
|
|
|
|
@fancy
|
|
|
|
|
def f(s: str) -> str:
|
|
|
|
|
return s
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A decorator with an argument also works:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def fancy(message: str) -> Callable[[Callable[[str], str]], Callable[[int], str]]:
|
|
|
|
|
def wrapper(func: Callable[[str], str]) -> Callable[[int], str]:
|
|
|
|
|
def wrapped_func(n: int) -> str:
|
|
|
|
|
print(message)
|
|
|
|
|
return func(str(n))
|
|
|
|
|
return wrapped_func
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
@fancy("so fancy")
|
|
|
|
|
def f(s: str) -> str:
|
|
|
|
|
return s
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
And a [generic
|
|
|
|
|
decorator](https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators)
|
|
|
|
|
that operates on functions of arbitrary signatures can be written
|
|
|
|
|
[with a `cast`](https://github.com/python/mypy/issues/1927) if the
|
|
|
|
|
output signature is always the same as the input signature:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
FuncT = TypeVar("FuncT", bound=Callable[..., object])
|
|
|
|
|
|
|
|
|
|
def fancy(func: FuncT) -> FuncT:
|
|
|
|
|
def wrapped_func(*args: object, **kwargs: object) -> object:
|
|
|
|
|
print("so fancy")
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
|
return cast(FuncT, wrapped_func) # https://github.com/python/mypy/issues/1927
|
|
|
|
|
|
|
|
|
|
@fancy
|
|
|
|
|
def f(s: str) -> str:
|
|
|
|
|
return s
|
|
|
|
|
```
|
|
|
|
|
|
2021-09-08 00:23:24 +02:00
|
|
|
|
(A generic decorator with an argument would return
|
|
|
|
|
`Callable[[FuncT], FuncT]`.)
|
2021-03-12 03:41:18 +01:00
|
|
|
|
|
|
|
|
|
But Mypy doesn't yet support the advanced type annotations that would
|
|
|
|
|
be needed to correctly type generic signature-changing decorators,
|
|
|
|
|
such as `zerver.decorator.authenticated_json_view`, which passes an
|
2021-08-20 21:53:28 +02:00
|
|
|
|
extra argument to the inner function. For these decorators we must
|
2021-03-12 03:41:18 +01:00
|
|
|
|
unfortunately give up some type safety by falling back to
|
|
|
|
|
`Callable[..., T]`.
|
|
|
|
|
|
2019-03-02 18:28:09 +01:00
|
|
|
|
## Troubleshooting advice
|
|
|
|
|
|
|
|
|
|
All of our linters, including mypy, are designed to only check files
|
2022-02-28 22:15:06 +01:00
|
|
|
|
that have been added in Git (this is by design, since it means you
|
2021-08-20 21:53:28 +02:00
|
|
|
|
have untracked files in your Zulip checkout safely). So if you get a
|
2019-03-02 18:28:09 +01:00
|
|
|
|
`mypy` error like this after adding a new file that is referenced by
|
|
|
|
|
the existing codebase:
|
|
|
|
|
|
2021-08-20 07:09:04 +02:00
|
|
|
|
```console
|
2020-06-29 23:36:12 +02:00
|
|
|
|
mypy | zerver/models.py:1234: note: Import of 'zerver.lib.markdown_wrappers' ignored
|
2019-03-02 18:28:09 +01:00
|
|
|
|
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.
|