snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
561
docs/testing/mypy.md
Normal file
561
docs/testing/mypy.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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 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.
|
||||
|
||||
- Our [best practices](#best-practices) section below.
|
||||
|
||||
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.
|
||||
|
||||
## Running mypy on Zulip's code locally
|
||||
|
||||
To run mypy on Zulip's python code, you can run the command:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```python
|
||||
foo = 1
|
||||
foo = '1'
|
||||
```
|
||||
|
||||
you'll get an error like this:
|
||||
|
||||
```console
|
||||
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 `pyproject.toml` 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 `pyproject.toml` 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.
|
||||
|
||||
## 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 channels 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
|
||||
first type parameter.
|
||||
|
||||
```python
|
||||
def foo(user: QuerySet[UserProfile]) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
For `.values_list`, supply the type of the column as the second type
|
||||
parameter.
|
||||
|
||||
```python
|
||||
def get_book_page_counts() -> QuerySet[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
|
||||
class BookMetadata(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
def get_book_meta_data(
|
||||
book_ids: List[int],
|
||||
) -> QuerySet[Book, BookMetadata]:
|
||||
return Book.objects.filter(id__in=book_ids).values("name", "id")
|
||||
```
|
||||
|
||||
When writing a helper function that returns the response from a test
|
||||
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")
|
||||
```
|
||||
|
||||
We sometimes encounter inaccurate type annotations in the Django
|
||||
stubs project. We prefer to address these by [submitting a pull
|
||||
request](https://github.com/typeddjango/django-stubs/pulls) to fix the
|
||||
issue in the upstream project, just like we do with `typeshed` bugs.
|
||||
|
||||
## 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)` validates that something is a
|
||||
`List[ResultT]` as follows:
|
||||
|
||||
```python
|
||||
@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`.
|
||||
|
||||
**Warning:** Mypy only checks the body of an overloaded function
|
||||
against the final signature and not against the more restrictive
|
||||
`@overload` signatures. This allows some type errors to evade
|
||||
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
|
||||
it's absolutely necessary. Consider writing multiple functions with
|
||||
different names instead.
|
||||
|
||||
See the [mypy overloading documentation][mypy-overloads] for more details.
|
||||
|
||||
[mypy-overloads]: https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading
|
||||
|
||||
## Best practices
|
||||
|
||||
### When is a type annotation justified?
|
||||
|
||||
Usually in fully typed code, mypy will protect you from writing a type
|
||||
annotation that isn't justified by the surrounding code. But when you
|
||||
need to write annotations at the border between untyped and typed
|
||||
code, keep in mind that **a type annotation should always represent a
|
||||
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`
|
||||
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
|
||||
caller already has it. (The validators in `zerver/lib/validator.py`
|
||||
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
|
||||
no effort to check that the input and output types match. So using
|
||||
`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:
|
||||
|
||||
- To annotate a dictionary where different keys correspond to values
|
||||
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).
|
||||
|
||||
- If you're annotating a class or function that might be used with
|
||||
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.
|
||||
|
||||
- If you need to accept data of several specific possible types at a
|
||||
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
|
||||
`str`, mypy requires that you validate it with an
|
||||
`instance(value, str)` test.
|
||||
|
||||
- If you really have no information about the type of a value, use the
|
||||
**`object` type**. Since every type is a subtype of `object`, you
|
||||
can correctly annotate any value as `object`. The [difference
|
||||
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.
|
||||
|
||||
- A common way for `Any` annotations to sneak into your code is the
|
||||
interaction with untyped third-party libraries. Mypy treats any
|
||||
value imported from an untyped library as annotated with `Any`, and
|
||||
treats any type imported from an untyped library as equivalent to
|
||||
`Any`. Consider providing real type annotations for the library by
|
||||
[**writing a stub file**](#mypy-stubs-for-third-party-modules).
|
||||
|
||||
### Avoid `cast()`
|
||||
|
||||
The [`cast`
|
||||
function](https://mypy.readthedocs.io/en/stable/type_narrowing.html#casts) lets you
|
||||
provide an annotation that Mypy will not verify. Obviously, this is
|
||||
completely unsafe in general.
|
||||
|
||||
```python
|
||||
x = cast(str, 5) # oops
|
||||
print(x.strip()) # runtime error
|
||||
```
|
||||
|
||||
Instead of using `cast`:
|
||||
|
||||
- You can use a [variable
|
||||
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] = []
|
||||
```
|
||||
|
||||
- You can use an [`isinstance`
|
||||
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
|
||||
|
||||
Mypy allows you to ignore any type checking error with a
|
||||
[`# type: ignore`
|
||||
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
|
||||
as a bug in mypy itself. If there are no safe options for dealing
|
||||
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
|
||||
|
||||
- As mentioned
|
||||
[above](#using-overload-to-accurately-describe-variations), we
|
||||
**discourage writing overloaded functions** because their bodies are
|
||||
not checked against the `@overload` signatures.
|
||||
|
||||
- **Avoid `Callable[..., T]`** (with literal ellipsis `...`), since
|
||||
mypy cannot check the types of arguments passed to it. Provide the
|
||||
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)
|
||||
is for optional values, which are values that could be `None`. For
|
||||
example, `Optional[int]` is equivalent to `Union[int, None]`.
|
||||
|
||||
The `Optional` type is **not for optional parameters** (unless they
|
||||
are also optional values as above). This signature does not use the
|
||||
`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
|
||||
collection. For example:
|
||||
|
||||
- An include list where the default is to include everything should be
|
||||
`Optional` with default `None`.
|
||||
- An exclude list where the default is to exclude nothing should be
|
||||
non-`Optional` with default `[]`.
|
||||
|
||||
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`.
|
||||
|
||||
```python
|
||||
s: Optional[str]
|
||||
if not s: # bad: are we checking for None or ""?
|
||||
...
|
||||
if s is None: # good
|
||||
...
|
||||
```
|
||||
|
||||
### Read-only types
|
||||
|
||||
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
|
||||
that was passed to it as an argument, especially by accident. To
|
||||
avoid this, prefer annotating function parameters with read-only
|
||||
types:
|
||||
|
||||
- [`Sequence`](https://docs.python.org/3/library/typing.html#typing.Sequence)
|
||||
instead of `List`,
|
||||
- [`Mapping`](https://docs.python.org/3/library/typing.html#typing.Mapping)
|
||||
instead of `Dict`,
|
||||
- [`AbstractSet`](https://docs.python.org/3/library/typing.html#typing.AbstractSet)
|
||||
instead of `Set`.
|
||||
|
||||
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)
|
||||
```
|
||||
|
||||
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)
|
||||
types might be appropriate. (But don’t use `Iterable` for a value
|
||||
that might be iterated multiple times, since a one-use iterator is
|
||||
`Iterable` too.)
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
confusion. But a read-only return type might be more appropriate for
|
||||
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
|
||||
```
|
||||
|
||||
(A generic decorator with an argument would return
|
||||
`Callable[[FuncT], FuncT]`.)
|
||||
|
||||
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
|
||||
extra argument to the inner function. For these decorators we must
|
||||
unfortunately give up some type safety by falling back to
|
||||
`Callable[..., T]`.
|
||||
|
||||
## 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:
|
||||
|
||||
```console
|
||||
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.
|
||||
Reference in New Issue
Block a user