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. basedpyright is 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:

  1. 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.
  2. Translate config deliberately, not field by field. Strictness levels do not map one to one. pyrefly init does a first pass for Pyrefly.
  3. Freeze existing errors with a baseline (Pyrefly, basedpyright) or targeted type: ignore[...] comments, so the gate catches only new problems.
  4. Run both checkers in CI for a while, the old one blocking, the new one advisory.
  5. 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