You start a project of your own and want a type checker in CI. Three are in common use:
- mypy is the oldest. This site grades every Python exercise with
mypy --strict. - Pyright comes from Microsoft. In VS Code, the Python extension installs the Pylance extension by default, and Pylance runs Pyright. So you may already see its errors in your editor.
- ty is the newest of the three. It comes from Astral, the company behind Ruff and uv.
All three read the same annotations, but they give different answers on the same file:
def broken():
return 1 + "row"
Plain mypy reports nothing, because by default it skips the body of a
function that has no annotations. mypy --strict reports two errors:
broken.py:1: error: Function is missing a return type annotation [no-untyped-def]
broken.py:2: error: Unsupported operand types for + ("int" and "str") [operator]
Pyright and ty check the body with no options set, and both report the bad addition. In their default modes they do not ask you to add annotations.
The checkers catch different bugs
No checker is a stricter version of another. Here mypy --strict passes code
that Pyright rejects. A TypedDict describes a dict with fixed string keys,
and NotRequired marks a key that may be missing:
from typing import NotRequired, TypedDict
class User(TypedDict):
name: str
nickname: NotRequired[str]
def display_name(user: User) -> str:
return user["nickname"]
mypy --strict reports no errors. Pyright reports that nickname may be
missing, and it is right. display_name({"name": "Ada"}) raises KeyError.
Write user.get("nickname", user["name"]) instead.
When you leave out a type, each checker infers one. For values = [1, "row"],
mypy infers list[object], so it rejects values[0] + 1. The checkers do not
all use the same rules for these guesses, so code that leans on an inferred
type can pass one checker and fail another. If the type matters, write it
down, and every checker starts from the same type:
values: list[int | str] = [1, "row"]
Try each one on your own code
Try all three on your real project before you choose:
- Install the project's dependencies, then run each checker over the whole project.
- Read the errors. Count the real bugs each checker finds, not the total number of errors.
- Check your libraries. Some, such as Django through the django-stubs package, use a mypy plugin to describe their behavior. Pyright and ty cannot load mypy plugins.
- Time a full CI run, and the delay in your editor after you change a line. A speed comparison on someone else's project does not predict yours.
If you are trying ty, also check that it handles every typing feature your code uses.
Make one checker the required check
For most projects, pick one checker to block merges in CI. Pin its version and upgrade it on purpose, because new releases add checks. Use whichever checker you like in your editor for quick feedback.
Each checker's strict mode is its own list of checks. Pyright's strict mode does
not make it agree with mypy --strict. If your editor runs Pyright and CI runs
mypy, expect them to disagree now and then. The concurrency track's
Fix Disagreements Between mypy and Pyright
works through such a disagreement in async code and shows how to find its
cause.
Run more than one checker in CI when you publish a typed library, because your users may run any of them.
Check your understanding Your code shows no errors from Pyright in VS Code. Will it pass the type check on this site? Show answer Hide answer
Not necessarily. When you submit, the site runs mypy --strict, and only its
errors count. Pyright's default mode checks different things. For example,
mypy --strict rejects a function with no annotations, and Pyright's default
mode accepts it. Read the mypy errors in your submission results and fix
those.
Going further
SQLAlchemy 2.0 has typed APIs that work with any checker, and it has deprecated its mypy plugin. An old tutorial that says SQLAlchemy needs the mypy plugin is out of date.
Read the mypy command-line options, the Pyright configuration and the ty documentation.