Skip to content
Published on

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

Authors

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.

CheckerOriginImplementationPrimary target2026 state
MypyDropbox / Python foundationPython (compiled to C via mypyc)OSS standard, library authorsStable, reference
PyrightMicrosoftTypeScript / NodeIDE (Pylance), CIVery active
PylanceMicrosoft (Pyright-based)TypeScript / NodeVS Code onlyPyright IDE wrapper
PyreMetaOCamlInstagram monorepoMaintenance mode
PyreflyMetaRustMeta monorepo, OSSOSS in 2025, fast-growing
tyAstralRustuv ecosystem usersAlpha as of 2025, accelerating
PytypeGooglePythonGoogle internalDeclining, 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.

PEPYearWhatPython version
4842014Type hints3.5
5262016Variable annotations3.6
5442017Protocols (structural typing)3.8
5612017Distributing type info (py.typed)3.7
5852019Builtin generics (list[int])3.9
5912019Final qualifier3.8
5932019Annotated3.9
6042019X pipe Y union syntax3.10
6122019ParamSpec3.10
6462020TypeVarTuple3.11
6472021TypeGuard3.10
6732022Self type3.11
6752022LiteralString3.11
6812022dataclass_transform3.11
6922023TypedDict for kwargs3.12
6952022New generic syntax (class Box[T]:)3.12
6962022TypeVar defaults3.13
6982022@override decorator3.12
7022023@deprecated decorator3.13
7052023TypedDict readonly3.13
7122023dataclass field transforms(proposed)
7282023TypedDict closed/extra_items(proposed)
7422024TypeIs (improved narrowing)3.13
7492024Annotation evaluation spec(in discussion)
7502024t-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 @override decorator. 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.

  1. Reference behavior. It is the reference implementation of the typing PEPs. Other checkers are measured against mypy's behavior.
  2. Plugin ecosystem. Plugins exist for Django, SQLAlchemy, attrs, and other libraries with heavy dynamic behavior.
  3. Incremental cache. The .mypy_cache/ directory makes the second run dramatically faster.
  4. mypyc self-compilation. Mypy itself ships compiled with mypyc, giving a noticeable speed boost.

Weaknesses.

  1. Speed. Cold start is slow on monorepos. Three to five minutes is common at 500k lines.
  2. PEP catch-up. PEP 695 took a while; PEP 742 (TypeIs) also lagged.
  3. 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.

  1. Rust native. Goal: 30 to 100 times faster cold start than mypy.
  2. Incremental DB. Built on Salsa, the incremental computation framework that powers rust-analyzer.
  3. LSP and CLI as first-class citizens. Like Pyright, IDE integration is baseline.
  4. 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.

  1. Monorepo friendly. Distributed indexing, incremental checking, and symbol DB validated at monorepo scale.
  2. Type-checks at scale. Battle-tested on a 100M-line codebase inside Meta.
  3. 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.

  1. Strong inference on un-annotated legacy code.
  2. Automatic .pyi stub generation.
  3. Long Google internal validation.

Weaknesses.

  1. Declining usage. Even inside Google, some teams have migrated to Pyright or Pyrefly.
  2. Slow PEP catch-up. New syntax like PEP 695 and PEP 742 lags.
  3. 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:

CheckerSoundnessCold startIncrementalLSPError messagesPlugins
MypyReferenceSlowAverageNeeds daemonLong, denseRich
PyrightVery strongFastVery fastFirst-classShort, clearLimited
PyreflyStrongVery fastVery fastFirst-classClearIn progress
tyIncompleteVery fastVery fastFirst-classClearNone

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_defs
  • disallow_any_generics
  • disallow_untyped_calls
  • disallow_incomplete_defs
  • disallow_untyped_decorators
  • check_untyped_defs
  • no_implicit_optional
  • warn_redundant_casts
  • warn_return_any
  • warn_unused_ignores
  • strict_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.

  1. Pyright stays the IDE standard. Pylance's VS Code share is already overwhelming. No real challenger.
  2. Mypy survives as the reference. The plugin ecosystem is too big. It does not die.
  3. ty grows fastest. Astral's track record (ruff and uv) buys trust. Alpha to beta to stable likely within a year.
  4. Pyrefly settles into the monorepo niche. It is OSS, but its market does not overlap with ty.
  5. Pytype stays in maintenance mode. Almost no new adoption.
  6. 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

Typing PEPs

Practical tools

Background