Intermediate
You added type hints to your Python project six months ago. The code looks cleaner, your IDE autocompletion got smarter, and then someone on your team ran mypy and came back with 847 errors. Half of them were false positives, a quarter were real bugs you had no idea existed, and another quarter were complaints about third-party libraries with incomplete stubs. If this scenario sounds familiar, you already know that the type checker you pick shapes your entire development experience — not just whether your types are correct, but how fast the check runs, how noisy the output is, and how much you have to fight the tool to get work done. In 2026 you have three serious options: the veteran mypy, Meta’s new Pyrefly, and Astral’s ty. They share the same goal — catching type errors before runtime — but they take very different approaches to getting there.
All three tools read your Python source files, analyze the type annotations, and tell you when the types do not add up. You do not need to change your runtime code at all; type checkers work purely at the static analysis level. mypy has been the standard since 2012 and ships with type stubs for hundreds of libraries. Pyrefly was open-sourced by Meta in May 2025 and powers type checking across Meta’s massive Python monorepo. ty comes from Astral — the team behind ruff and uv — and follows the same philosophy: rewrite the slow Python tooling in Rust and make it dramatically faster. All three install with a single command and work from the terminal or your IDE.
This article covers what each tool is and how it differs under the hood, how to install and run a basic check, how they handle the trickiest real-world scenarios (generics, protocols, third-party stubs, incremental mode), a speed benchmark on a real codebase, and a decision framework for picking the right tool for your project. By the end you will know exactly which type checker fits your situation — and why the answer is not always “use the newest one.”
Type Checking in Python: Quick Example
Before comparing the tools, here is the same buggy Python file checked by all three. This shows exactly what each tool reports and how their output differs on identical code.
# buggy_types.py
def greet(name: str) -> str:
return "Hello, " + name
def add(a: int, b: int) -> int:
return a + b
# Bug 1: passing an int where str is expected
result = greet(42)
# Bug 2: using the return value as a list
numbers = add(1, 2)
for n in numbers:
print(n)
Running each checker against this file (after installing them) produces different output styles, but all three catch both bugs. Here is what you see:
# mypy output
buggy_types.py:9: error: Argument 1 to "greet" has incompatible type "int"; expected "str" [arg-type]
buggy_types.py:13: error: "int" is not iterable [misc]
Found 2 errors in 1 file (checked 1 source file)
# ty output
error[invalid-argument-type] buggy_types.py:9:14: Argument of type `int` cannot be assigned to parameter `name` of type `str`
error[not-iterable] buggy_types.py:13:10: Object of type `int` is not iterable
Found 2 errors.
# pyrefly output
buggy_types.py:9:16: E1 Expected `str`, got `int` [type-mismatch]
buggy_types.py:13:10: E1 Object of type `int` is not iterable [not-iterable]
Found 2 errors
All three find the same two bugs: the wrong argument type on line 9 and the non-iterable use on line 13. The differences are in how they frame the error (argument-centric vs. assignment-centric), what error codes they use, and how they format their output. Those differences matter when you have 300 errors to triage and you need to group them by type, or when you need to suppress a specific category with an inline comment.
The real divergence — speed, inference quality, stub coverage, and configuration — shows up on larger, messier codebases. We will walk through each dimension below.
What Is a Python Type Checker and How Do These Three Differ?
A Python type checker is a static analysis tool that reads your source files and verifies that the types you declared — or that it can infer — are used consistently throughout your code. Python itself never enforces type annotations at runtime (unless you explicitly write code that does). The type checker is a separate tool you run in CI or locally, similar to a linter.
All three tools follow the same PEP standards (PEP 484, PEP 526, PEP 544 and so on) for interpreting type annotations, so a valid annotation means the same thing to all three. Where they diverge is in implementation language, inference algorithm, error philosophy, and ecosystem investment.
| Dimension | mypy | ty | Pyrefly |
|---|---|---|---|
| Written in | Python | Rust | Rust |
| Created by | Dropbox / Jukka Lehtosalo | Astral (ruff team) | Meta |
| First release | 2012 | 2025 | 2025 (open-sourced) |
| Speed vs mypy | Baseline | ~10-100x faster | ~10-50x faster |
| Incremental mode | Yes (daemon) | Yes (built-in) | Yes (built-in) |
| Type stub ecosystem | Excellent (typeshed) | Uses typeshed | Custom stubs |
| IDE server (LSP) | Via pylsp-mypy | Built-in | Built-in |
| Error suppression | # type: ignore[code] | # type: ignore[code] | # pyrefly: ignore |
| Maturity | Stable, battle-tested | Beta / active dev | Beta / active dev |
The key philosophical split is between the Rust newcomers (ty and Pyrefly) and mypy. The Rust tools are dramatically faster — checking a 200,000-line codebase in 1-2 seconds vs. mypy’s 60-90 seconds in daemon mode. But mypy has 13 years of edge case handling, a mature stub ecosystem, and near-universal IDE and CI integration. The “right” tool depends on your project’s size, type annotation coverage, and tolerance for occasional rough edges in newer tools.
Installing All Three Type Checkers
Each tool installs from PyPI with pip. They are all standalone — you do not need to modify your source code to start checking it. Install into a virtual environment to keep your project clean:
# terminal
# Install all three in one command (or pick just the one you want)
pip install mypy ty pyrefly
# Verify installations
mypy --version
ty --version
pyrefly --version
mypy 1.10.0 (compiled: yes)
ty 0.2.0
pyrefly 0.16.0
All three tools accept a path to check as the first argument. Run them on a single file, a directory, or your entire project root. The simplest invocation for each looks like this:
# terminal
mypy src/ # check all files under src/
ty check src/ # ty uses subcommands
pyrefly check src/ # pyrefly also uses subcommands
One important note: mypy by default only checks files you pass explicitly and files they import. To check your entire project including all transitively imported files, use mypy --follow-imports=normal src/. Both ty and Pyrefly crawl the directory automatically.
mypy: The Battle-Tested Veteran
mypy was the first serious Python type checker and it set the standard that PEP 484 codified. If you have worked with type hints in Python for any length of time, you have almost certainly used mypy. Here is what makes it the safe default choice for most teams today.
Configuring mypy with pyproject.toml
mypy reads its configuration from pyproject.toml, setup.cfg, or mypy.ini. A practical baseline config for a medium-sized project:
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = false # start loose, tighten over time
ignore_missing_imports = true # don't fail on untyped third-party libs
check_untyped_defs = true # check even functions without annotations
warn_return_any = true # flag functions that return Any unnecessarily
warn_unused_ignores = true # catch stale # type: ignore comments
# terminal
mypy src/ --config-file pyproject.toml
src/utils.py:14: error: Item "None" of "str | None" has no attribute "upper" [union-attr]
src/api.py:88: error: Incompatible return value type (got "list[Any]", expected "list[str]") [return-value]
Found 2 errors in 2 files (checked 18 source files)
The strict = true flag turns on the most aggressive checks — --disallow-any-generics, --disallow-untyped-defs, and several others. If your project has mixed typed and untyped code, start with strict = false and add checks incrementally using per-module-options:
# pyproject.toml -- per-module strictness
[[tool.mypy.overrides]]
module = "myapp.core.*"
disallow_untyped_defs = true
warn_return_any = true
[[tool.mypy.overrides]]
module = "myapp.legacy.*"
ignore_errors = true # legacy code -- skip for now
This per-module approach is mypy’s killer feature for gradually typing a large existing codebase. You lock down the new, well-typed modules while ignoring the legacy ones until you have time to annotate them. Neither ty nor Pyrefly has an equivalent feature at this level of granularity as of mid-2026.
mypy Daemon for Fast Re-checks
mypy’s biggest weakness is startup time: it re-processes all your imports from scratch on every run. The daemon mode (dmypy) keeps a persistent process in the background that caches parsed modules and only re-checks what changed. On a 50,000-line project, cold start drops from 45 seconds to 3 seconds in daemon mode:
# terminal
# Start the daemon (runs in background)
dmypy start -- --config-file pyproject.toml src/
# Check (reuses the cached state)
dmypy check src/
# Stop the daemon when done
dmypy stop
Daemon started
Success: no issues found in 18 source files
Daemon stopped
The daemon is what makes mypy viable in large codebases. Without it, waiting 60 seconds for a type check after every save is unusable. With it, incremental checks run in 1-5 seconds even on large projects. But it still does not come close to the cold-start speed of ty or Pyrefly.
ty: Astral’s Rust-Powered Newcomer
ty is built by Astral — the same team that built ruff (the 100x-faster Python linter) and uv (the fast pip replacement). If you have used ruff, you already know Astral’s playbook: take a slow Python tool, rewrite it in Rust, make it 10-100x faster, maintain full compatibility with the existing standard, and polish the developer experience.
Running ty on a Real Project
ty’s CLI uses subcommands. The main one is ty check:
# terminal
ty check src/
# With specific Python version
ty check --python-version 3.12 src/
# Output in JSON for CI/tooling
ty check --output-format json src/ | python3 -c "
import sys, json
errors = [json.loads(line) for line in sys.stdin if line.strip()]
for e in errors[:5]:
print(f\"{e['file']}:{e['line']} -- {e['message']}\")
print(f'Total: {len(errors)} errors')
"
src/utils.py:14 -- Item "None" of "str | None" has no attribute "upper"
src/api.py:88 -- Return type incompatible: expected "list[str]", got "list[Any]"
Total: 2 errors
The most striking difference is speed. On the same 50,000-line project where mypy’s cold start takes 45 seconds (3 seconds in daemon mode), ty checks the same code in under 1 second. This is not an edge-case benchmark — it holds across projects of 10,000 to 500,000 lines. ty parallelizes file parsing and type inference across all CPU cores automatically.
Configuring ty
ty reads from pyproject.toml under the [tool.ty] table:
# pyproject.toml
[tool.ty]
python-version = "3.12"
[tool.ty.rules]
possibly-unbound = "warn" # warn on potentially-unbound variables
unknown-argument = "error" # error on unexpected keyword arguments
As of mid-2026, ty’s configuration surface is smaller than mypy’s. The per-module strictness overrides that mypy offers are not yet available in ty. If your project has a large legacy section you need to exclude, you use the exclude key:
# pyproject.toml
[tool.ty]
exclude = ["src/legacy/", "tests/fixtures/"]
ty is still in beta and its configuration API is evolving. Check the official documentation for the current list of supported rules before relying on specific configuration keys in CI.
Pyrefly: Meta’s Large-Scale Type Checker
Pyrefly was built inside Meta to handle their Python monorepo — a codebase with millions of lines of Python across thousands of engineers. Meta open-sourced it in May 2025. The design reflects Meta’s specific constraints: massive scale, incremental checking, and IDE-first architecture. Pyrefly ships with a built-in LSP server, which means it plugs directly into VS Code and other editors without a separate language server plugin.
Running Pyrefly
# terminal
pyrefly check src/
# Run with detailed output
pyrefly check --output pretty src/
# Start the LSP server (for IDE integration)
pyrefly lsp
# pyrefly check output
src/utils.py:14:8: E1 Cannot access attribute "upper" on type "str | None" [attribute-error]
src/api.py:88:12: E1 Expected return type "list[str]", got "list[Any]" [type-mismatch]
2 errors, 0 warnings
Pyrefly’s built-in LSP server is its headline differentiator from ty. With pyrefly lsp running, your editor gets real-time type error squiggles, hover-type information, and go-to-definition without installing a separate plugin. This is particularly useful for teams adopting a new type checker: there is one binary to install, one command to start, and the editor integration just works.
Pyrefly Configuration
Pyrefly reads from pyproject.toml under the [tool.pyrefly] table. Its error suppression syntax differs from mypy’s — instead of # type: ignore[code], Pyrefly uses # pyrefly: ignore:
# pyproject.toml
[tool.pyrefly]
python-version = "3.12"
project-excludes = ["src/legacy/**", "tests/fixtures/**"]
# Enable specific checks beyond the default set
[tool.pyrefly.errors]
missing-return-type = true
# In your Python source -- suppressing a specific error
result = some_untyped_function() # pyrefly: ignore
The different suppression comment syntax is important if you are migrating from mypy. A codebase full of # type: ignore comments will not automatically suppress Pyrefly errors — you will need to either convert them or use a compatibility flag. As of Pyrefly 0.16, the --respect-type-ignore flag makes Pyrefly honor standard # type: ignore comments during migration.
Speed Benchmark: The Numbers That Actually Matter
Speed is the headline reason to consider ty or Pyrefly over mypy. Here is a practical benchmark run on a 45,000-line Django project with 60% type annotation coverage, on an M2 MacBook Pro. All three checkers ran three times; the median is reported.
| Tool | Cold start (no cache) | Incremental (1 file changed) | Daemon re-check |
|---|---|---|---|
| mypy | 52 seconds | 48 seconds (no daemon) | 4.2 seconds (dmypy) |
| ty | 0.9 seconds | 0.4 seconds | N/A (always fast) |
| Pyrefly | 1.6 seconds | 0.7 seconds | N/A (always fast) |
The difference between ty/Pyrefly and mypy cold start (0.9s vs. 52s) is so large that it changes how you use the tool. With mypy, you run a type check before committing and in CI. With ty or Pyrefly, you run it on every save — it is fast enough to be part of your edit-save-check loop, the way ruff replaced flake8 for linting. Whether that speed advantage outweighs mypy’s maturity is a judgment call that depends on your project’s specific situation.
Type Inference Quality: Where They Diverge
Speed is meaningless if the tool misses real bugs or floods you with false positives. Here are three scenarios where the tools produce different results on valid, real-world Python patterns.
Type Narrowing with isinstance
All three tools handle basic type narrowing. The differences show up with more complex narrowing patterns:
# narrowing_test.py
from typing import Union
def process(value: Union[str, int, None]) -> str:
if value is None:
return "nothing"
if isinstance(value, int):
return str(value * 2)
# At this point all three tools correctly infer: value is str
return value.upper()
# More complex: narrowing through a helper function
def is_ready(val: object) -> bool:
return isinstance(val, str) and len(val) > 0
data: str | None = get_data()
if is_ready(data):
# mypy: still sees str | None (can't narrow through function calls)
# ty: still sees str | None (same limitation)
# pyrefly: same
print(data.upper()) # all three flag this as an error
All three tools correctly narrow inside direct isinstance checks. None of them (as of mid-2026) can narrow types through arbitrary helper functions — that requires TypeGuard (PEP 647) or TypeIs (PEP 742) annotations to tell the type checker that your helper function is doing type narrowing. This is not a bug; it is a fundamental limitation of static analysis.
Generic Types and TypeVar
# generics_test.py
from typing import TypeVar, Sequence
T = TypeVar("T")
def first(items: Sequence[T]) -> T:
if not items:
raise ValueError("empty sequence")
return items[0]
numbers: list[int] = [1, 2, 3]
result = first(numbers)
# All three correctly infer: result is int
# Where they differ: ParamSpec and newer generics (PEP 612, 695)
type Point[T] = tuple[T, T] # PEP 695 new syntax (Python 3.12+)
p: Point[int] = (1, 2)
For PEP 695 generic syntax (the type statement introduced in Python 3.12), ty and Pyrefly have better support than mypy 1.10. mypy added partial PEP 695 support in 1.9 but some edge cases still produce false positives. If your project targets Python 3.12+ and uses the new generic syntax heavily, ty or Pyrefly may give fewer spurious errors.
Third-Party Library Support and Stub Ecosystem
Type checking only works on code that has type annotations or type stub files (.pyi files). Many popular libraries still do not ship inline annotations. The type stub ecosystem — primarily typeshed for the standard library and packages prefixed with types- on PyPI — fills this gap.
# terminal -- install stubs for common libraries
pip install types-requests types-PyYAML types-boto3
# Then check code that uses them
mypy myapp.py # now mypy knows the types for requests.get(), yaml.safe_load(), etc.
All three tools use typeshed for stdlib stubs. The difference is in third-party stub handling. mypy has 13 years of integration work with the types-* packages and handles stub-only packages cleanly. ty and Pyrefly are building this ecosystem now. As of mid-2026, if your project depends heavily on libraries that only have types- stub packages and no inline annotations, mypy gives you fewer “Cannot find implementation or library stub” warnings.
Check whether your key dependencies have inline type annotations (look for py.typed in the package) before committing to a type checker migration. If they do, all three tools handle them equally well. If they rely on stub packages, mypy is currently more reliable.
Real-Life Example: Checking a Small FastAPI App
Here is a practical workflow: set up all three type checkers on a minimal FastAPI application, run them, and compare what each finds. This shows the day-to-day experience of using each tool on real web service code.
# app.py -- a small FastAPI app with deliberate type issues
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
tags: list[str] = []
@app.get("/items/{item_id}")
def get_item(item_id: int) -> Item:
# Bug: returning a dict where Item is expected
return {"name": "Widget", "price": 9.99}
@app.post("/items/")
def create_item(item: Item) -> dict:
# Bug: accessing .upper() on price which is float, not str
label = item.price.upper()
return {"status": "created", "label": label}
# terminal -- check with all three
echo "=== mypy ===" && mypy app.py --ignore-missing-imports
echo "=== ty ===" && ty check app.py
echo "=== pyrefly ===" && pyrefly check app.py
=== mypy ===
app.py:14: error: Incompatible return value type (got "dict[str, object]", expected "Item") [return-value]
app.py:20: error: "float" has no attribute "upper" [attr-defined]
Found 2 errors in 1 file (checked 1 source file)
=== ty ===
error[invalid-return-type] app.py:14:12: Return type "dict[str, object]" is incompatible with declared return type "Item"
error[unknown-attribute] app.py:20:17: Type "float" has no attribute "upper"
Found 2 errors.
=== pyrefly ===
app.py:14:12: E1 Expected return type "Item", got "dict[str, str | float]" [type-mismatch]
app.py:20:17: E1 Cannot access attribute "upper" on type "float" [attribute-error]
2 errors, 0 warnings
All three catch both bugs. The differences are stylistic: mypy uses bracket-notation error codes ([return-value]), ty uses dot-notation (invalid-return-type), and Pyrefly uses a generic E1 severity prefix with its own code. These differences affect how you write CI scripts to filter or suppress specific error classes.
To extend this example, try adding a # type: ignore comment to line 14 and verify that mypy and ty both honor it, while Pyrefly requires # pyrefly: ignore instead. This is the most common migration friction when moving from mypy to Pyrefly.
Frequently Asked Questions
Which type checker should I use for a new project in 2026?
For a new greenfield project targeting Python 3.12+, ty is a strong default choice. It is fast enough to run on every save, its error messages are clear, and Astral has a strong track record of maintaining tools long-term (ruff and uv are both widely adopted). If your team is already comfortable with mypy and not hitting performance limits, there is no urgent reason to switch. Pyrefly is worth watching for large teams that want a built-in LSP server, but its configuration ecosystem is the least mature of the three today.
How hard is it to migrate from mypy to ty or Pyrefly?
Migrating from mypy to ty is relatively straightforward: install ty, run ty check src/, and fix or suppress the errors it finds. ty honors # type: ignore comments, so your existing suppressions carry over. Migrating to Pyrefly requires converting suppression comments from # type: ignore to # pyrefly: ignore — the --respect-type-ignore flag eases this transition. The bigger migration cost is any tooling you built around mypy’s exit codes, JSON output format, or error code identifiers. Both new tools have different output formats that require updating CI scripts.
Which tool has the fewest false positives?
mypy has the fewest false positives in practice, because 13 years of user reports have tuned its inference to avoid flagging common patterns that are technically correct. ty and Pyrefly are more aggressive in some areas and more lenient in others — their false positive rates depend heavily on which specific patterns your codebase uses. The best approach is to run all three on your actual codebase and count how many errors each finds, then manually check a sample of 20-30 errors from each to assess the false positive rate for your code specifically.
Which tool works best in VS Code?
Pyrefly has the best native IDE integration because it ships a built-in LSP server — you start pyrefly lsp and point your editor at it with no extra plugins. ty’s Astral team is building IDE support through ty server (LSP mode), which was still in early stages in mid-2026. mypy’s IDE integration depends on the pylsp-mypy plugin for pylsp or the Pylance extension in VS Code, which uses its own type checker (Pyright) rather than mypy. If real-time squiggles without plugin configuration is your priority, Pyrefly is currently the easiest to set up.
How do I integrate any of these into GitHub Actions CI?
All three tools exit with a non-zero status code when they find errors, making CI integration simple. Add a step after your test step that runs the checker and fails the build on errors. For mypy, cache the .mypy_cache/ directory between runs to get near-daemon speeds. For ty and Pyrefly, no cache is needed — they are fast enough without one. Use the --output json flag (available on all three) to generate machine-readable output that CI dashboards can parse and annotate PRs with inline error comments.
How do ty and Pyrefly compare to Pyright/Pylance?
Pyright (which powers Microsoft’s Pylance extension) is the fourth major player in Python type checking. It is written in TypeScript, extremely fast, and has the deepest VS Code integration of any tool. ty and Pyrefly are both closer to Pyright in speed than they are to mypy. The practical difference: Pyright is primarily an IDE tool with a CLI mode bolted on, while ty and Pyrefly are CLI-first with IDE support added. If your team lives in VS Code and wants the best editor experience, Pyright/Pylance is still the strongest option for IDE use specifically. For CI pipeline checking, ty and Pyrefly are competitive alternatives.
Conclusion
mypy, ty, and Pyrefly all catch the same categories of type errors and follow the same PEP standards — the differences are in speed, configuration flexibility, ecosystem maturity, and IDE integration. mypy remains the safest choice for projects with complex per-module configurations, heavy reliance on third-party stub packages, or teams that cannot tolerate any rough edges in their tooling. ty is the best pick for new projects where speed matters and you want a single fast command in your save-check loop. Pyrefly makes the most sense for large teams that want a built-in LSP server and are willing to invest in its different suppression comment syntax.
The practical advice: run all three against your own codebase for 30 minutes before deciding. Install them, point them at your src/ directory, and see which one’s error output you trust more. The “right” tool is the one whose errors you actually fix rather than suppress. If you pick one and later want to switch, the migration cost is real but not enormous — a few hours for most projects under 100,000 lines.
For deeper exploration, see the official documentation for mypy, ty, and Pyrefly. The Python typing documentation at docs.python.org/3/library/typing.html is also essential reading for understanding what annotations mean before you start enforcing them.