- Published on
Python Type Checkers in 2026 — Mypy, Pyright (Pylance), Pyre, Pyrefly, ty: Deep-Dive Comparison
- Authors

- Name
- Youngju Kim
- @fjvbn20031
Prologue — Python has too many type checkers in 2026
Python started as a dynamic language. But in 2026, writing Python in industry effectively means writing type annotations. FastAPI defines routes through type hints. Pydantic validates data through types. SQLAlchemy 2.0's ORM declares columns with Mapped[int]. Modern Python libraries are hard to use without types.
The question is: who checks those types? And between 2025 and 2026, the answer to that question got suddenly complicated.
- Mypy — the reference implementation since 2012. Built by Jukka Lehtosalo (originally his PhD work) with Guido van Rossum at Dropbox.
- Pyright — announced in 2019 by Microsoft. The engine behind Pylance in VS Code. Strongest IDE feedback.
- Pyre — Meta's OCaml-based checker, built for the Instagram monorepo. In decline.
- Pyrefly — 2025 Rust rewrite of Pyre. Fast at monorepo scale.
- ty — 2025 Rust-based checker from Astral (the uv and ruff company). Alpha, but fast.
- Pytype — Google's inference-heavy checker. Declining usage.
This post maps these tools as of May 2026: who builds what, how soundness vs speed vs ergonomics trade off, which PEPs each tool tracks, and how to roll out type checking gradually on a real codebase.
1. The 2026 Python type-checker map
Big picture first. Each checker differs in origin, implementation language, and primary target.
| Checker | Origin | Implementation | Primary target | 2026 state |
|---|---|---|---|---|
| Mypy | Dropbox / Python foundation | Python (compiled to C via mypyc) | OSS standard, library authors | Stable, reference |
| Pyright | Microsoft | TypeScript / Node | IDE (Pylance), CI | Very active |
| Pylance | Microsoft (Pyright-based) | TypeScript / Node | VS Code only | Pyright IDE wrapper |
| Pyre | Meta | OCaml | Instagram monorepo | Maintenance mode |
| Pyrefly | Meta | Rust | Meta monorepo, OSS | OSS in 2025, fast-growing |
| ty | Astral | Rust | uv ecosystem users | Alpha as of 2025, accelerating |
| Pytype | Python | Google internal | Declining, maintenance only |
Two big trends pop out.
First, the Rust rewrite wave. The Python toolchain has been moving to Rust for three years. Ruff (linter) and uv (package manager) made Astral's reputation. Now type checkers are following. ty and Pyrefly are the examples. Mypy already self-compiles via mypyc (a Python-to-C compiler), but Rust-native is still faster.
Second, the IDE vs CLI split. Pyright and Pylance shine at LSP, incremental checking, and watch mode. Mypy shines at batch CI checking. Many teams now use both. ty and Pyrefly aim at both markets simultaneously.
2. Python typing PEP timeline — the big changes
To compare type checkers you first need to know what they should check. Here are the major PEPs.
| PEP | Year | What | Python version |
|---|---|---|---|
| 484 | 2014 | Type hints | 3.5 |
| 526 | 2016 | Variable annotations | 3.6 |
| 544 | 2017 | Protocols (structural typing) | 3.8 |
| 561 | 2017 | Distributing type info (py.typed) | 3.7 |
| 585 | 2019 | Builtin generics (list[int]) | 3.9 |
| 591 | 2019 | Final qualifier | 3.8 |
| 593 | 2019 | Annotated | 3.9 |
| 604 | 2019 | X pipe Y union syntax | 3.10 |
| 612 | 2019 | ParamSpec | 3.10 |
| 646 | 2020 | TypeVarTuple | 3.11 |
| 647 | 2021 | TypeGuard | 3.10 |
| 673 | 2022 | Self type | 3.11 |
| 675 | 2022 | LiteralString | 3.11 |
| 681 | 2022 | dataclass_transform | 3.11 |
| 692 | 2023 | TypedDict for kwargs | 3.12 |
| 695 | 2022 | New generic syntax (class Box[T]:) | 3.12 |
| 696 | 2022 | TypeVar defaults | 3.13 |
| 698 | 2022 | @override decorator | 3.12 |
| 702 | 2023 | @deprecated decorator | 3.13 |
| 705 | 2023 | TypedDict readonly | 3.13 |
| 712 | 2023 | dataclass field transforms | (proposed) |
| 728 | 2023 | TypedDict closed/extra_items | (proposed) |
| 742 | 2024 | TypeIs (improved narrowing) | 3.13 |
| 749 | 2024 | Annotation evaluation spec | (in discussion) |
| 750 | 2024 | t-string (typed string literal) | (proposed) |
The headline summary.
- PEP 695 (3.12): new generic syntax. Type parameters now live next to the class:
class Box[T]:. Looks more like TypeScript. - PEP 696 (3.13): TypeVar defaults. You can write
Box[T = int]. - PEP 742 (3.13): TypeIs fixes a TypeGuard weakness — narrowing now works in both branches.
- PEP 698 (3.12): the
@overridedecorator. Verifies that a method actually overrides a parent method. - PEP 749: cleans up annotation evaluation rules. Defines what comes after
from __future__ import annotations.
Different checkers track this list at different speeds. Pyright is fastest, Mypy is slowest. ty and Pyrefly implement new PEPs from day one.
3. Mypy — the reference implementation
Mypy started in 2012, originally as Jukka Lehtosalo's PhD research. Guido van Rossum joined him at Dropbox and it took off. As of 2026 it is effectively the reference implementation of PEP 484.
# mypy example
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
if user_id < 0:
return None
return "alice"
name = find_user(42)
print(name.upper()) # mypy error: name may be None
Running mypy on the snippet above gives you:
example.py:9: error: Item "None" of "Optional[str]" has no attribute "upper"
Found 1 error in 1 file (checked 1 source file)
Strengths.
- Reference behavior. It is the reference implementation of the typing PEPs. Other checkers are measured against mypy's behavior.
- Plugin ecosystem. Plugins exist for Django, SQLAlchemy, attrs, and other libraries with heavy dynamic behavior.
- Incremental cache. The
.mypy_cache/directory makes the second run dramatically faster. - mypyc self-compilation. Mypy itself ships compiled with mypyc, giving a noticeable speed boost.
Weaknesses.
- Speed. Cold start is slow on monorepos. Three to five minutes is common at 500k lines.
- PEP catch-up. PEP 695 took a while; PEP 742 (TypeIs) also lagged.
- Error messages. "Argument 2 to ... has incompatible type" lines tend to be long and dense.
A typical mypy.ini:
[mypy]
python_version = 3.13
strict = True
plugins = pydantic.mypy, sqlalchemy.ext.mypy.plugin
[mypy-tests.*]
disallow_untyped_defs = False
strict = True essentially turns on all hardness flags. The usual pattern: new code starts strict; legacy code gets opt-outs per module.
4. Pyright + Pylance — Microsoft, strongest IDE feedback
Pyright launched in 2019 from Microsoft. It is written in TypeScript and runs on Node.js. Inside VS Code it ships as the engine behind Pylance.
The Pyright design goal: incremental, immediate, accurate. Save a file and the result should arrive in 0.1 seconds. To pull this off Pyright does:
- AST caching. Files that did not change are not re-parsed.
- Per-symbol incremental. When one function changes, only its dependencies are rechecked.
- Aggressive inference. More aggressive than mypy in many cases (especially type narrowing).
# Pyright narrowing example
from typing import Union
def process(x: Union[int, str]) -> str:
if isinstance(x, int):
# x narrows to int here (mypy also does this)
return str(x * 2)
# x narrows to str here (mypy also does this)
return x.upper()
Basic narrowing is universal. The difference shows up in subtler cases.
# Pyright narrows here, mypy sometimes does not
from typing import Literal
def get_status() -> Literal["ok", "error"]:
...
s = get_status()
if s == "ok":
# Pyright: s is Literal["ok"]
# mypy: same in modern versions
handle_ok()
Pyright vs Pylance.
- Pyright: open source. Usable from CLI, LSP, CI.
- Pylance: VS Code only, closed source. Pyright engine plus VS Code integrations like auto-import, docstring inlay hints, and session cache.
Install Pyright via npm:
npm install -g pyright
pyright src/
Configuration lives in pyrightconfig.json or [tool.pyright] in pyproject.toml:
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__"],
"typeCheckingMode": "strict",
"pythonVersion": "3.13",
"reportMissingImports": "error",
"reportUnknownArgumentType": "warning"
}
typeCheckingMode has four levels: off, basic, standard, strict. strict is usually tighter than mypy's --strict.
5. Astral's ty — Rust-based, from the ruff and uv team
Astral rewrote the Python toolchain between 2024 and 2025. Once Ruff (linter) and uv (package manager) had proven the "Rust is dramatically faster" thesis, the same team announced ty, a Rust-based type checker, in alpha in 2025.
Design principles.
- Rust native. Goal: 30 to 100 times faster cold start than mypy.
- Incremental DB. Built on Salsa, the incremental computation framework that powers rust-analyzer.
- LSP and CLI as first-class citizens. Like Pyright, IDE integration is baseline.
- Tight integration with ruff. Same workspace config and same cache directory.
As of May 2026 ty is still alpha, but new PEPs land fast. PEP 695 was supported from launch. PEP 742 and PEP 749 are already in.
# install
uv tool install ty
# check
ty check src/
# start LSP server
ty server
Configuration lives in [tool.ty] inside pyproject.toml:
[tool.ty]
python-version = "3.13"
strict = true
exclude = ["tests/"]
[tool.ty.rules]
unresolved-import = "error"
unused-ignore-comment = "warning"
The threat ty poses.
- The speed gap is huge. A 500k-line monorepo that takes 3 minutes on mypy can drop to ~5 seconds on ty even in alpha.
- For ruff and uv users, the appeal is one toolchain: same company, same cache, same config.
- But there is no plugin system yet. Dynamic libraries like SQLAlchemy 2.0 and Pydantic 2 are hard to cover with pure static analysis.
Reality check, May 2026. ty is great for new projects and small libraries. Pointing it at a 50k-line SQLAlchemy 1.x codebase still produces a lot of false positives. The consensus is it will stabilize over the next year.
6. Meta's Pyrefly — the Rust rewrite of Pyre
Meta runs a Python monorepo north of 100M lines across Instagram and Facebook backends. Pyre was Meta's OCaml-based checker built for that monorepo. As the OCaml ecosystem narrowed and the monorepo's incremental and distributed needs grew, Meta started Pyrefly between 2024 and 2025 — a Rust rewrite.
Pyrefly's strengths.
- Monorepo friendly. Distributed indexing, incremental checking, and symbol DB validated at monorepo scale.
- Type-checks at scale. Battle-tested on a 100M-line codebase inside Meta.
- OSS. Released as OSS in 2025. Usable outside Meta.
# install
cargo install pyrefly
# or
pip install pyrefly
# check
pyrefly check src/
Pyrefly config lives in pyrefly.toml or [tool.pyrefly] in pyproject.toml:
[tool.pyrefly]
python_version = "3.13"
project_root = "."
search_path = ["src", "lib"]
strict = true
Pyrefly vs ty positioning.
- Pyrefly: monorepo scale first. Validation inside Meta is the strength.
- ty: general OSS and small-to-medium projects first. The ruff and uv ecosystem fit is the strength.
Both are Rust, but they target different markets. Small library authors lean ty. Monorepo operators lean Pyrefly.
7. Google's Pytype — declining but still around
Pytype is Google's checker. It was active between 2015 and 2017 and stood out for inferring types without annotations.
# Pytype can infer types even without annotations
def add(x, y):
return x + y
# Pytype: tracks usage sites and infers int/str/float possibilities
add(1, 2) # int + int -> int
add("a", "b") # str + str -> str
Strengths.
- Strong inference on un-annotated legacy code.
- Automatic
.pyistub generation. - Long Google internal validation.
Weaknesses.
- Declining usage. Even inside Google, some teams have migrated to Pyright or Pyrefly.
- Slow PEP catch-up. New syntax like PEP 695 and PEP 742 lags.
- Speed. Some reports say it is slower than mypy on monorepos.
As of 2026, Pytype is not a great choice for new adoption. But some large projects (Apache Beam, parts of Google) still use it, so it will not disappear soon.
8. Soundness vs Speed vs Ergonomics — four-way comparison
Three axes matter when picking a checker.
- Soundness: the breadth of bugs caught. "How often does mypy let through code that breaks at runtime?"
- Speed: both cold start and incremental.
- Ergonomics: error messages, plugins, LSP, documentation.
Subjective ratings as of May 2026:
| Checker | Soundness | Cold start | Incremental | LSP | Error messages | Plugins |
|---|---|---|---|---|---|---|
| Mypy | Reference | Slow | Average | Needs daemon | Long, dense | Rich |
| Pyright | Very strong | Fast | Very fast | First-class | Short, clear | Limited |
| Pyrefly | Strong | Very fast | Very fast | First-class | Clear | In progress |
| ty | Incomplete | Very fast | Very fast | First-class | Clear | None |
Theoretical soundness is anchored by mypy. Practical soundness diverges on narrowing, overloads, and protocols.
Speed is dominated by Pyrefly and ty. A million-line codebase: 5 minutes on mypy, 30 seconds on Pyright, ~5 seconds on Pyrefly or ty.
Error messages are best in Pyright. Mypy is verbose. Pyrefly and ty are clear but still early.
Plugins are mypy's strongest moat. That said, the new tools support Pydantic 2 and SQLAlchemy 2.0 well out of the box because they were designed against modern library shapes.
9. PEP 695, TypeIs, Self, Never — what you hit in real code
Compared to PEP 484 days, 2026 Python code has noticeably different syntax. The pieces you actually meet.
PEP 695 — new generic syntax (3.12+)
Old syntax (still works):
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, item: T) -> None:
self.item = item
New syntax (3.12+):
class Box[T]:
def __init__(self, item: T) -> None:
self.item = item
# functions too
def first[T](items: list[T]) -> T:
return items[0]
# type aliases too
type StringList = list[str]
It looks more like TypeScript now. Pyright supported it on day one. Mypy 1.4 had partial support; 1.7 stabilized it. ty and Pyrefly have shipped it from launch.
PEP 742 — TypeIs (3.13+)
The fix for TypeGuard's narrowing weakness.
from typing import TypeIs
def is_str_list(val: list[object]) -> TypeIs[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_str_list(items):
# items narrows to list[str]
print(",".join(items))
else:
# items stays list[object] — but TypeGuard would have lost info here
print("not strings")
The key difference: TypeIs narrows in both branches. The "what it is not" survives in the else branch — something TypeGuard could not do.
Self type (PEP 673, 3.11+)
from typing import Self
class Builder:
def with_name(self, name: str) -> Self:
self.name = name
return self
def with_age(self, age: int) -> Self:
self.age = age
return self
class FancyBuilder(Builder):
def with_color(self, color: str) -> Self:
self.color = color
return self
# Self keeps the subclass type through chaining
b = FancyBuilder().with_name("alice").with_color("red")
# b: FancyBuilder, not Builder, thanks to Self
What used to require a TypeVar trick is now one word: Self.
Never type
from typing import Never
def fail(msg: str) -> Never:
raise RuntimeError(msg)
def process(x: int | str) -> str:
if isinstance(x, int):
return str(x)
if isinstance(x, str):
return x
fail("unreachable") # code after this is flagged as dead
Never declares "this function does not return". The checker uses it for dead-code analysis.
@override decorator (PEP 698, 3.12+)
from typing import override
class Base:
def greet(self) -> str:
return "hello"
class Child(Base):
@override
def greet(self) -> str:
return "hi"
@override
def greeet(self) -> str: # typo — no such method on Base, checker flags it
return "hi"
Same role as TypeScript's override. If the parent has no method with that name, the checker errors out.
10. Strict mode and gradual adoption strategy
Strict from day one is great when you can do it. On an existing codebase you can't. The standard gradual rollout.
Step 1 — establish a baseline
First, snapshot the current state.
mypy --ignore-missing-imports src/ > baseline.txt
wc -l baseline.txt
That number is the starting point. The rule for new PRs: the number does not grow.
Step 2 — strict for new code
In mypy.ini, apply strict only to new modules.
[mypy]
python_version = 3.13
[mypy-myapp.new_module.*]
strict = True
[mypy-myapp.legacy.*]
ignore_errors = True
New code tightens from day one. Legacy is ignored for now.
Step 3 — --strict-optional first
The biggest single ROI is distinguishing Optional[X] from X. Turn this on before other strict flags.
[mypy]
strict_optional = True
no_implicit_optional = True
These two flags alone catch about 80% of None-related bugs.
Step 4 — disallow_untyped_defs
Forbid untyped function definitions.
[mypy]
disallow_untyped_defs = True
disallow_incomplete_defs = True
New functions must be typed. Existing functions get annotated gradually.
Step 5 — strict = True
All strict flags at once. The final stage.
[mypy]
strict = True
What strict = True turns on.
disallow_untyped_defsdisallow_any_genericsdisallow_untyped_callsdisallow_incomplete_defsdisallow_untyped_decoratorscheck_untyped_defsno_implicit_optionalwarn_redundant_castswarn_return_anywarn_unused_ignoresstrict_equality
11. Real setup — pre-commit, CI, pytest integration
pre-commit hooks
.pre-commit-config.yaml:
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies:
- "pydantic>=2.0"
- "sqlalchemy>=2.0"
args: ["--strict"]
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.380
hooks:
- id: pyright
Running both checkers is common. Mypy for depth, Pyright for speed.
GitHub Actions CI
.github/workflows/typecheck.yml:
name: typecheck
on: [pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -e ".[dev]"
- run: mypy --strict src/
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -e ".[dev]"
- run: pip install pyright
- run: pyright src/
pytest-mypy-plugins — test the types themselves
A tool for testing whether types behave as intended.
# tests/typing/test_types.yml
- case: dict_lookup
main: |
from typing import TypedDict
class User(TypedDict):
name: str
age: int
user: User = {"name": "alice", "age": 30}
reveal_type(user["name"]) # N: Revealed type is "builtins.str"
reveal_type(user["age"]) # N: Revealed type is "builtins.int"
Drop yaml like this in and pytest runs mypy to check that reveal_type output matches expected.
inline_snapshot — snapshot tests for type output
Snapshot the output of reveal_type.
from inline_snapshot import snapshot
def test_types():
from mymodule import process
result = process(42)
assert type(result) == snapshot(int)
Type changes show up as PR diffs, intentional or not.
mypy daemon (dmypy)
The way to push incremental speed on a large codebase.
# start daemon
dmypy start -- --strict
# check (cache alive)
dmypy check src/
# status
dmypy status
# stop
dmypy stop
VS Code extensions for mypy can run dmypy automatically. Still slower than Pyright's LSP, but much faster than batch mypy.
12. Decision guide — which one for your team
By situation.
Case 1 — new OSS library.
- Main: Pyright (in CI)
- Secondary: Mypy (validate plugin compatibility at publish)
- Why: Pyright is fast and strong, but OSS consumers check with mypy, so both should pass.
Case 2 — Pydantic + FastAPI backend.
- Main: Pyright + Pylance (VS Code)
- Secondary: Mypy with pydantic.mypy plugin
- Why: Pydantic 2 fits Pyright well. Mypy needs the plugin.
Case 3 — SQLAlchemy 2.0 + Django monorepo.
- Main: Mypy with sqlalchemy.ext.mypy.plugin and django-stubs
- Secondary: Pyright (IDE only)
- Why: The SQLAlchemy 1.x to 2.0 migration leans on the mypy plugin. Pyright misses some ORM patterns.
Case 4 — new project, Astral toolchain.
- Main: ty
- Secondary: Pyright (CI hardening)
- Why: If you already use ruff and uv, ty fits naturally. Alpha, but on a green project the false-positive rate is lower.
Case 5 — 100M-line monorepo.
- Main: Pyrefly
- Secondary: none
- Why: Validated at that scale inside Meta. Other checkers OOM.
Case 6 — legacy Python 2/3 mixed code.
- Main: Pytype (short-term)
- Migrate to Mypy or Pyright afterwards
- Why: Pytype's inference of un-annotated code is strong. Not recommended for green-field adoption.
13. 2026 predictions — who survives a year from now
Closing forecast for next year.
- Pyright stays the IDE standard. Pylance's VS Code share is already overwhelming. No real challenger.
- Mypy survives as the reference. The plugin ecosystem is too big. It does not die.
- ty grows fastest. Astral's track record (ruff and uv) buys trust. Alpha to beta to stable likely within a year.
- Pyrefly settles into the monorepo niche. It is OSS, but its market does not overlap with ty.
- Pytype stays in maintenance mode. Almost no new adoption.
- Pyre effectively ends. A full migration to Pyrefly is probable within a year.
The biggest wildcard is ty's plugin system. If ty handles SQLAlchemy 2.0 and Django well out of the box, mypy's last moat falls. Or ty and mypy could share a plugin compatibility layer. Either way, the May 2027 landscape will look different again.
14. References
Official docs
- Mypy documentation
- Pyright documentation
- Pylance — VS Code Marketplace
- Pyre
- Pyrefly (Meta)
- ty (Astral)
- Pytype (Google)
Typing PEPs
- PEP 484 — Type Hints
- PEP 526 — Variable Annotations
- PEP 544 — Protocols
- PEP 561 — Distributing and Packaging Type Information
- PEP 585 — Builtin Generic Types
- PEP 604 — Union Operator
- PEP 612 — ParamSpec
- PEP 646 — TypeVarTuple
- PEP 673 — Self Type
- PEP 681 — dataclass_transform
- PEP 695 — Type Parameter Syntax
- PEP 696 — TypeVar Defaults
- PEP 698 — Override Decorator
- PEP 702 — Deprecated Decorator
- PEP 705 — TypedDict ReadOnly
- PEP 742 — TypeIs
- PEP 749 — Annotation Evaluation
Practical tools
- pytest-mypy-plugins
- pydantic.mypy plugin
- sqlalchemy.ext.mypy.plugin
- django-stubs
- typeshed (stubs for the standard library)