Type checking#
napari is type-checked with pyrefly. The check runs on every pull request and has to pass before we merge, but you are not expected to fix type errors in your own PR. If it fails and the fix isn’t obvious, ask for help and a team member will be eager to guide you.
Note
Why we moved off mypy, and what we compared against, is in the Island Dispatch post From Any to Certainty.
Running the check#
tox -e pyrefly
This builds a small environment from resources/requirements_pyrefly.txt
constraints with tox. A run takes about a second with a cached environment.
tox -e pyrefly defaults to pyrefly check, and arguments after --
are appended, so any pyrefly subcommand can also work:
tox -e pyrefly -- check --count-errors
tox -e pyrefly -- suppress --remove-unused=all
Don’t run pyrefly from your development environment (e.g.
uv run pyrefly check): it resolves the packages you happen to have installed
rather than the pinned ones, and reports a different set of errors. In
addition, the code editor integration reads
the same [tool.pyrefly] configuration, but it sees your environment too — so
treat editor suggestions as a hint and tox -e pyrefly as the standard.
Annotating new code#
Annotate the parameters and return values you are adding or changing, and leave unrelated types alone — a bug fix shouldn’t turn into a typing sweep. Only modify previous annotations if they are relevant to the contribution you are making.
When feasible, add
from __future__ import annotationsat the top of any module that doesn’t have it yet. This makes annotations lazy, so they are not executed at runtime until required, in order to infer the types. Where this is not possible (see warning below), annotations that are not used in the code should be wrapped in quotes to avoid early evaluation.Import annotation-only types under
if TYPE_CHECKING:, so heavy imports and potential import cycles stay out of the runtime path. Ruff rules flag these and will often move them for you with pre-commit. To do this, addfrom typing import TYPE_CHECKINGto the top of the file, then wrap the imports in:if TYPE_CHECKING: from some_module import SomeType
Important
Lazy evaluation of annotation breaks tools that require runtime type inspection, such as magicgui decorators and Pydantic models. For those, type annotations must be real imports!
Run
tox -e pyreflyand read the first complaint before the rest; later errors are often consequences of the first one.If something can’t be fixed honestly, suppress it by error kind and say why, as described below.
Choosing types#
Annotate parameters and return values, not local variables. They are the contract other modules rely on, and locals are inferred well enough without help.
Parameters should accept the widest type that works; returns give back the concrete type you built. When psosible, take
Iterable,SequenceorMappingrather thanlistordict, and hand back thelistordictyou actually made.Anyswitches off checking for everything it touches. It is sometimes the honest answer for a dynamic corner of the code, but it should be a decision rather than a way to make one error disappear.Prefer narrowing to
cast().typing.castonly tells the checker to believe you, so if it is the only option left, keep it as narrow as you can and say in a comment what makes it safe.Type arrays only with what the contract needs. The full annotation for a NumPy array is
np.ndarray, for examplenp.ndarray[tuple[int, ...], np.dtype[np.float64]], which pins both the shape and the dtype. If the shape is not part of the contract, usenumpy.typing’snpt.NDArrayalias instead, which leaves the shape unspecified but the dtype specified, for examplenpt.NDArray[np.float64]. If neither shape nor dtype is part of the contract, usenp.ndarraywithout any arguments. For docstrings, always use the runtime class namenp.ndarrayeven where the annotation isnpt.NDArray. Read more in the NumPy typing guide, including aboutnpt.ArrayLikefor inputs that can be converted to arrays.
When the check complains#
Suppressing should be rare. Most types can be written honestly, and a suppression in the wrong place hides a real bug. Occasionally one genuinely will not resolve, because of a third-party stub, a gap in the typing specification, or dynamic behaviour — that is what suppressions are for. The important thing is to name the error kind, so a reader can tell why it was needed and pyrefly can check that the suppression is still doing something. The last word of a pyrefly message is the rule it applied:
ERROR src/napari/utils/tree/node.py:64:31-53: Argument `int | None` is not assignable
to parameter `object` with type `int` in function `list.insert` [bad-argument-type]
^^^^^^^^^^^^^^^^^
Put that rule on the offending line:
indices.insert(0, item.index_in_parent()) # pyrefly: ignore[bad-argument-type]
A bare # pyrefly: ignore silences every diagnostic on the line, including
ones a future pyrefly release adds, and nothing can check it for staleness.
The pyrefly config sets unused-ignore as an error, so a suppression that
stops matching anything fails the build. To clear the ones your change left
behind, run tox -e pyrefly -- suppress --remove-unused=all.
Modifying the type checking config#
Don’t widen
project-excludesto make a failure go away. Adding a module takes it out of the check for everyone. Suppress narrowly instead, and if you believe a module really can’t be checked, raise it in the pull request rather than adding it quietly.Bringing a module back into the check is a self-contained task and a good first contribution: delete its entry from
project-excludesinpyproject.toml, runtox -e pyrefly, fix what you can, suppress what you cannot with codes, then run it again and drop any suppression the change leaves unused. For example,utilsare friendlier starting points than the Qt widget code.
What the check does not cover#
The environment is deliberately small — it installs
resources/requirements_pyrefly.txt and nothing else — so imports like
vispy, scipy, pandas, dask, zarr and their kind resolve to Any.
project-excludes also takes many files under src/napari out of the check,
tests included, such that zero errors means only that pyrefly found nothing
wrong with the type it could see.
Where things live#
What |
Where |
|---|---|
Checker config |
|
Pinned checker version and its dependencies |
|
Constraints pins regeneration |
|
Tox environment |
|
CI job |
|
Where to read more#
Typing for Python Developers — pyrefly’s own five-minute tour, if type hints are new to you.
Static type checking — the Scientific Python guide’s chapter, written for scientists and research software engineers. Its “loose vs. specific types” section is the long version of the “accept the widest type that works” point above.
Type hints cheat sheet — syntax suggestions to copy. It is written for mypy, but the syntax is the same.