For most of the last decade, choosing a Python type checker was a short conversation: mypy if you wanted the reference implementation, Pyright if you wanted speed and a good editor experience. That is no longer true. Two Rust-based checkers, Astral's ty and Meta's Pyrefly, both reached public beta at the end of 2025, and both are fast enough to change how you think about running a type checker on every keystroke.
More choice also means more confusion. The four tools read the same annotations but do not agree on everything: how much they infer, what they do with unannotated code, what their error codes are called, and how you silence them.
This post is a cheatsheet for picking one, configuring it, and running it in CI. Every output shown below comes from running the same file through the versions that were current on the date of this post. I avoid speed rankings from my own laptop; the numbers quoted are each project's own claims, and you should benchmark on your codebase.
1. The four checkers at a glance
| mypy | Pyright | ty | Pyrefly | |
|---|---|---|---|---|
| Maintainer | Python community (created by Jukka Lehtosalo, long developed at Dropbox) | Microsoft | Astral (makers of uv and Ruff) | Meta |
| Written in | Python, compiled with mypyc | TypeScript (runs on Node) | Rust | Rust |
| Latest version (March 2026) | 1.19.1 (December 2025) | 1.1.408 (January 2026) | 0.0.21 (March 2026) | 0.55.0 (March 2026) |
| Status | Stable, reference implementation | Stable | Beta since December 2025 | Beta since November 2025 |
| Install | pip install mypy |
npm i -g pyright or pip install pyright |
uv tool install ty |
pip install pyrefly |
| Run | mypy src/ |
pyright |
ty check |
pyrefly check |
| Editor | Via third-party extensions; daemon (dmypy) for speed |
Powers Pylance in VS Code; LSP for any editor | Built-in language server, VS Code extension | Built-in language server, VS Code extension |
| Plugins | Yes (Pydantic, django-stubs and others) | No plugin system | No plugin system | No plugins; preliminary built-in Django and Pydantic support |
| Default for unannotated functions | Bodies not checked, return is Any |
Checked, return type inferred | Checked, return type Unknown (in 0.0.21) |
Checked, return type inferred in many cases |
A few points behind the table:
- mypy is still the reference. Many typing PEPs are prototyped there first, and its plugin system is a big reason many Django and Pydantic codebases stay on it. Version 1.18 shipped performance work that the release notes put at about 40% faster than 1.17 on mypy's own codebase.
- Pyright is the mature fast option. If you use VS Code with Pylance, you already run Pyright's engine.
basedpyrightis a community fork that adds stricter rules and baseline files; it tracks Pyright releases. - ty and Pyrefly are betas. Their own announcements say so. Both are usable for editor feedback today; treat CI gating on them as an experiment you can back out of.
2. What actually differs
All four implement the same typing specification, so the differences show up at the edges. These are the ones that matter in practice.
| Area | mypy | Pyright | ty | Pyrefly |
|---|---|---|---|---|
Unannotated def |
Skipped unless check_untyped_defs = true |
Checked | Checked | Checked |
| Return inference | None for unannotated functions (Any) |
Infers from body, even per call site | Not for unannotated functions in 0.0.21 | Infers for many functions |
| Empty container then append | Infers list[int] from the first append |
list[Unknown] |
list[Unknown] |
Infers list[int] from first use |
| Strictness levels | Flags, or --strict bundle |
off, basic, standard (default), strict |
Per-rule severity (ignore, warn, error) |
Per-error toggles and severities |
| Error identifiers | Codes like [union-attr] |
Rules like reportOptionalMemberAccess |
Rules like unresolved-attribute |
Codes like [missing-attribute] |
# type: ignore |
Respected, codes matched | Respected (configurable) | Respected, but suppresses every rule on the line | Respected |
| Tool-specific ignore | (uses type: ignore[code]) |
# pyright: ignore[rule] |
# ty: ignore[rule] |
# pyrefly: ignore[code] |
| Baseline of existing errors | No built-in baseline | No (basedpyright has one) | No | Yes (--baseline) |
| Migration helper | n/a | n/a | n/a | pyrefly init reads mypy or Pyright config |
The gradual typing philosophy differs, and that is what surprises people when they switch. mypy assumes that an unannotated function is code you have not opted into yet, so it stays quiet. Pyright, ty and Pyrefly check every function body by default. On a legacy codebase, that one difference can turn "0 errors" into several hundred.
3. Same file, four checkers
Here is a small file with three real bugs and one unannotated legacy function:
from dataclasses import dataclass
@dataclass
class User:
id: int
email: str | None = None
def domain(user: User) -> str:
return user.email.split("@")[1] # bug 1: email can be None
def total(prices: list[float]) -> int:
return sum(prices) # bug 2: sum of floats is not an int
def legacy(x):
return x + 1
legacy("a") # runtime TypeError, no annotations
count: int = "3" # bug 3: wrong type
All four checkers find the three annotated bugs with default settings. None of them flags legacy("a") by default, because x has no annotation. What changes is the wording and the error code:
| Bug | mypy 1.19.1 | Pyright 1.1.408 | ty 0.0.21 | Pyrefly 0.55.0 |
|---|---|---|---|---|
email may be None |
union-attr |
reportOptionalMemberAccess |
unresolved-attribute |
missing-attribute |
sum of floats returned as int |
arg-type (reports the sum argument) |
reportReturnType |
invalid-return-type |
bad-return |
str assigned to int |
assignment |
reportAssignmentType |
invalid-assignment |
bad-assignment |
legacy("a") |
Only with --strict (no-untyped-def, no-untyped-call) |
Only in strict mode (reportMissingParameterType and related) |
Not reported | Not reported |
Two details worth noticing. mypy blames the argument to sum, the others blame the return, which is the same bug seen from different ends. And ty's output is the most verbose by design: it prints the source with the declared type and the offending value both underlined, in a style borrowed from the Rust compiler.
What they infer without annotations
Inference is where the tools disagree the most. The same snippet run through each:
from typing import reveal_type
def legacy(x):
return x + 1
def make():
return [1, 2, 3]
reveal_type(legacy(1))
reveal_type(make())
items = []
items.append(1)
reveal_type(items)
| Expression | mypy 1.19.1 | Pyright 1.1.408 | ty 0.0.21 | Pyrefly 0.55.0 |
|---|---|---|---|---|
legacy(1) |
Any |
Literal[2] |
Unknown |
Unknown |
make() |
Any |
list[int] |
Unknown |
list[int] |
items |
list[int] |
list[Unknown] |
list[Unknown] |
list[int] |
Any and Unknown behave the same way for checking: anything goes. The practical consequence is that the same codebase gets more checking from Pyright and Pyrefly than from mypy or ty, because they know more types without you writing them. ty is still young here, so expect this row to change between releases.
4. Configuration side by side
All four can live in pyproject.toml. A realistic starting point for a project on Python 3.12 with a src/ layout and some legacy code:
# mypy
[tool.mypy]
python_version = "3.12"
files = ["src", "tests"]
strict = true
warn_unused_ignores = true
[[tool.mypy.overrides]]
module = ["myapp.legacy.*"]
ignore_errors = true
[[tool.mypy.overrides]]
module = ["some_untyped_lib.*"]
ignore_missing_imports = true
# Pyright
[tool.pyright]
include = ["src", "tests"]
exclude = ["src/myapp/legacy"]
pythonVersion = "3.12"
typeCheckingMode = "strict"
reportUnnecessaryTypeIgnoreComment = "warning"
# ty
[tool.ty.environment]
python-version = "3.12"
[tool.ty.src]
include = ["src", "tests"]
[tool.ty.rules]
possibly-unresolved-reference = "warn"
unused-type-ignore-comment = "error"
[[tool.ty.overrides]]
include = ["src/myapp/legacy/**"]
[tool.ty.overrides.rules]
all = "ignore"
# Pyrefly
[tool.pyrefly]
project-includes = ["src", "tests"]
project-excludes = ["src/myapp/legacy/**"]
python-version = "3.12"
[tool.pyrefly.errors]
bad-assignment = true
unused-ignore = "warn"
unannotated-parameter = "warn"
Things to know about each:
| Tool | Config file options | Notes |
|---|---|---|
| mypy | mypy.ini, setup.cfg, pyproject.toml ([tool.mypy]) |
strict is a bundle of flags; you can enable it and then turn individual flags off. Per-module settings go in overrides. |
| Pyright | pyrightconfig.json or pyproject.toml ([tool.pyright]) |
typeCheckingMode sets defaults; any report* rule can be set to "none", "warning" or "error" on top. |
| ty | ty.toml or pyproject.toml ([tool.ty]), plus a user-level ty.toml |
ty.toml wins over pyproject.toml. If python-version is not set, ty reads project.requires-python. overrides apply rules by glob. |
| Pyrefly | pyrefly.toml or pyproject.toml ([tool.pyrefly]) |
[errors] maps error codes to on, off or a severity; opt-in codes such as unannotated-parameter add strictness. pyrefly init generates a config from an existing mypy or Pyright setup. |
5. Suppressing errors
Suppressions are where switching tools gets expensive, because each tool's own comment means nothing to the others.
# Works in all four (PEP 484 standard comment)
value = load() # type: ignore[assignment]
# Tool-specific, with rule names
user.email.split("@") # pyright: ignore[reportOptionalMemberAccess]
user.email.split("@") # ty: ignore[unresolved-attribute]
user.email.split("@") # pyrefly: ignore[missing-attribute]
# Whole file
# pyrefly: ignore-errors
| Rule of thumb | Why |
|---|---|
| Always include the code in brackets | A bare ignore hides the next, unrelated error on that line too |
| Turn on unused-ignore reporting | mypy warn_unused_ignores, Pyright reportUnnecessaryTypeIgnoreComment, ty unused-ignore-comment and unused-type-ignore-comment (both warn by default), Pyrefly unused-ignore. Stale ignores pile up fast. |
Prefer type: ignore[...] if you run two checkers |
Every tool understands it. Note that ty treats it as suppressing every rule on the line, codes or not. |
| Use a baseline for big adoptions | Pyrefly's pyrefly check --baseline=<file> --update-baseline records existing errors so only new ones fail. pyrefly suppress adds ignore comments instead. |
6. Running in CI
Pin the version. All four tools change their inference between releases, and an unpinned checker turns a dependency update into a red build nobody caused.
# .github/workflows/types.yml
name: types
on: [pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --locked
# Pick one primary gate:
- run: uv run mypy
# - run: uv run pyright
# - run: uvx [email protected] check --error-on-warning
# - run: uvx [email protected] check
| Practice | How |
|---|---|
| Check against the right environment | Run inside the project's virtual environment so third-party packages and their stubs resolve. Missing imports are the most common source of noise. |
| Pin in the lockfile | Add the checker as a dev dependency (uv add --dev mypy==1.19.1) rather than installing latest in CI. |
| One gate, one advisor | Gate merges on one checker. Run a second one as non-blocking if you want its signal. |
| Match the editor | If your team uses Pylance, CI on Pyright means what developers see is what CI sees. Same for ty or Pyrefly with their extensions. |
| Machine-readable output | pyright --outputjson, ty check --output-format, pyrefly check --output-format for annotations in pull requests. |
| Library authors | pyright --verifytypes yourpackage reports how complete your public API's types are. |
7. Speed: what the projects claim
Speed is the main reason people are looking at ty and Pyrefly, so here are the claims as each project published them. They are vendor benchmarks on vendor-chosen codebases.
| Claim | Source |
|---|---|
| ty is "between 10x and 60x faster than mypy and Pyright" on the command line without caching | Astral, ty beta announcement (December 2025) |
| After an edit in a large file of the PyTorch codebase, ty recomputed diagnostics in 4.7 ms vs. 386 ms for Pyright and 2.38 s for Pyrefly | Same announcement |
| Pyrefly beta cut type checking time on large projects with big dependency trees by over 95% compared with its own alpha | Meta, Pyrefly beta announcement (November 2025) |
| mypy 1.18 is about 40% faster than 1.17 on mypy's own code, sometimes 10x or more | mypy 1.18 release notes (September 2025) |
On a small or medium codebase, all four are fast enough for CI. The gap matters most in the editor on very large monorepos, which is exactly the setting Meta and Astral built for. Measure on your own repository, with a warm and a cold cache, before using speed as the deciding factor.
8. Recent changes worth knowing
Only what shipped by early March 2026:
| Tool | Change | When |
|---|---|---|
| mypy 1.19 | Last feature release supporting Python 3.9 (end of life October 2025); the binary fixed-format cache is no longer experimental (--fixed-format-cache); experimental PEP 747 TypeForm support behind --enable-incomplete-feature=TypeForm |
November 2025 |
| mypy 1.18 | Large performance work; fixed-format cache introduced as experimental | September 2025 |
| Pyright 1.1.408 | Latest release; ongoing typing spec updates | January 2026 |
| Pyrefly beta | Conformance with the typing spec up from 39% at alpha to over 70%; preliminary Django and Pydantic support with no plugins; Jupyter notebook support in the IDE; better pyrefly init migration |
November 2025 |
| ty beta | Full language server (go to definition, rename, completions, auto-import, inlay hints); intersection types; plan for a stable release in 2026 | December 2025 |
9. How to choose
| Situation | Reasonable choice | Why |
|---|---|---|
| Large existing mypy codebase with plugins (django-stubs, Pydantic plugin) | Stay on mypy | Plugins have no equivalent elsewhere yet. Re-test the others when you upgrade. |
| New project, VS Code team | Pyright (standard, then strict) | Same engine as Pylance; mature; strong inference. |
| Already all-in on uv and Ruff | Try ty in the editor, keep mypy or Pyright as the CI gate for now | Fast, good diagnostics, one vendor for the toolchain. Still beta. |
| Very large monorepo, editor latency is the pain | Pilot Pyrefly or ty in the editor | Both are built for this. Pyrefly's baseline helps adoption. |
| Untyped legacy code, want any signal | mypy (default) or Pyright basic |
mypy's quiet default on unannotated code avoids a wall of errors on day one. |
| Library you publish | Test with mypy and Pyright | Most of your users run one of those two. --verifytypes checks completeness. |
A migration in five steps, whichever direction you go:
- Run the new checker on a branch with no config and count errors by code. Most of the count usually comes from two or three codes.
- Translate config deliberately, not field by field. Strictness levels do not map one to one.
pyrefly initdoes a first pass for Pyrefly. - Freeze existing errors with a baseline (Pyrefly, basedpyright) or targeted
type: ignore[...]comments, so the gate catches only new problems. - Run both checkers in CI for a while, the old one blocking, the new one advisory.
- Flip the gate, remove the old tool, and clean up suppressions that only the old tool needed.
Why this matters
- Types are cheap checks that run before tests. All four tools caught all three annotated bugs in the sample with zero configuration.
- The defaults differ more than the engines. Unannotated code, inference and strictness explain most "it passes in one tool and fails in another" surprises.
- Speed changes how you use a checker. When diagnostics arrive in milliseconds, type checking moves from a CI step into the editing loop.
- Pin versions and prefer coded ignores. That keeps CI reproducible and keeps a future migration cheap.
- Betas are for trying, not yet for betting the gate on. Use ty and Pyrefly where a regression costs a little, and keep a stable checker where it costs a lot.
Final Thoughts
The Python type checking space has more serious options now than at any time since mypy started. That is good news: competition has already pushed mypy to get faster and is forcing everyone to be explicit about where they differ from the specification.
You do not need to pick a winner today. Keep the checker that gates your CI stable and pinned, try a new one in your editor for a week, and look at what it finds that the current one misses. That comparison on your own code is worth more than any benchmark table.
Sources
- mypy, Release notes (CHANGELOG) (September and November 2025): mypy 1.18 performance; mypy 1.19 Python 3.9 support ending, fixed-format cache, PEP 747
TypeForm. - PyPI, mypy, pyright, ty, pyrefly release histories (accessed for versions up to March 2026): mypy 1.19.1 (December 15, 2025), Pyright 1.1.408 (January 8, 2026), ty 0.0.21 (March 6, 2026), Pyrefly 0.55.0 (March 3, 2026).
- mypy, The mypy configuration file:
strict,overrides,warn_unused_ignores,check_untyped_defs. - Microsoft, Pyright configuration: type checking modes and
report*rules. - Astral, ty: An extremely fast Python type checker and language server (December 2025): beta status, speed claims, language server features.
- Astral, ty configuration and suppression comments: config files and precedence, rule severities, overrides,
ty: ignoreandtype: ignorebehavior. - Meta, Pyrefly Beta (November 2025): conformance from 39% to over 70%, performance, Django and Pydantic support,
pyrefly init. - Meta, Pyrefly configuration and error suppressions:
project-includes,[errors],pyrefly: ignore,pyrefly suppress, baseline files. - Python, Typing specification: type: ignore comments: the standard suppression comment all four tools read.