ty vs Pyrefly: which Python type checker should you pick?
ty (Astral) and Pyrefly (Meta) are the two new Rust-based Python type checkers. Both are fast. Both ship a built-in language server. Both read pyproject.toml. Choosing between them comes down to whether you want a tool that’s already at 1.0 with broad framework support, or a tool whose Pydantic defaults match yours.
This page is the recommendation, then the evidence behind it. For the wider field that includes mypy, pyright, Basedpyright, and Zuban, see How do Python type checkers compare?.
Pick Pyrefly, with one exception
Use Pyrefly for most Python projects today. Pick ty if your codebase is heavy on Pydantic models and you’d rather have one tool-level default than add model_config = ConfigDict(strict=True) to each model.
That’s the whole answer. The rest of the page is the evidence.
Important
Every Pyrefly example below assumes preset = "default" in your config. Pyrefly’s out-of-the-box preset for an unconfigured project is basic, which silences most type errors. Run pyrefly init first. See Watch out for Pyrefly’s basic preset for the full list.
Why Pyrefly wins by default
Four reasons, in order of how often they decide the call.
Pyrefly hit 1.0 stable; ty is still 0.0.x
Pyrefly reached 1.0.0 on 2026-05-12 and is the default type checker for Instagram engineers at Meta (~20M lines of Python). PyTorch and JAX adopted it during the beta. The 1.0 release commits to semantic versioning, which means no surprise breaking changes within the 1.x line.
ty is at version 0.0.50 as of June 2026. The ty Reference page confirms breaking changes can land between any two 0.0.x releases. Astral has signaled a 1.0 target for 2026 but not committed a date.
If “the type checker my CI depends on shipped a breaking change overnight” is a problem for your team, you want the tool that’s already at 1.0.
Pyrefly tracks the typing spec more closely
The Python Typing Council maintains the conformance test suite, which exercises edge cases of the typing spec. As of June 2026 the suite tests Pyrefly 1.1.0 and ty 0.0.50: Pyrefly passes over 90% of it, ahead of ty. The conformance dashboard shows the live per-section numbers.
ty’s gaps cluster in generics (TypeVarTuple, constrained TypeVar union solving), Protocols (ClassVar members, recursive generic protocols), and type aliases. For application code those gaps rarely show up. For libraries shipping type stubs to PyPI they show up the moment a downstream consumer hits one. Pyrefly’s types round-trip more reliably across consumers.
Track ty’s progress on its feature status issue.
Pyrefly translates mypy configs with one command
If you’re migrating from mypy, Pyrefly offers a CLI surface ty does not yet match:
pyrefly initreadsmypy.ini,setup.cfg, or[tool.mypy]inpyproject.tomland writes a translated config. When it detects a mypy config, it emits thelegacypreset, which disables three Pyrefly checks mypy lacks so the first run isn’t a flood of new diagnostics.pyrefly inferwrites type annotations directly into source files. See how to add type annotations with pyrefly infer.pyrefly suppressbulk-adds or removes ignore comments.permissive-ignores = truehonors leftover# mypy: ignore-errorscomments.
ty’s CLI is narrower: ty check, ty server, ty explain. The mypy-to-ty migration is a manual translation. Astral has signaled an automatic option is on the roadmap; it isn’t there yet.
Pyrefly type-checks Pydantic and Django out of the box
Pyrefly 1.0 ships built-in support for both frameworks: model validation, field types, autocomplete on model fields. Django gets model-field type awareness; Pyrefly knows the difference between a CharField and an IntegerField and types attribute access accordingly.
ty has no plugin system and no built-in framework awareness. It handles Pydantic through PEP 681 dataclass_transform generically, which works because Pydantic’s BaseModel carries the dataclass-transform marker. Django models have no such marker, so ty checks them as plain classes: queryset.filter(name__icontains=...) and Model.objects.create(**kwargs) calls fall back to looser typing.
For Django-heavy codebases this is the difference between catching a typo in a field name at type-check time and only finding it in a test run.
When ty is the better pick
ty applies PEP 681 dataclass_transform strictly to Pydantic models out of the box. Pyrefly’s built-in support models Pydantic’s lax-by-default runtime instead. The same constructor call produces different verdicts:
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int = 0
u = User(name="ada", age="forty")- ty reports
Argument is incorrect: Expected int, found Literal["forty"]. - Pyrefly reports
0 errors(lax-mode coercion is permitted statically).
Adding model_config = ConfigDict(strict=True) to the model flips Pyrefly to strict-mode checking, and both tools agree:
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(strict=True)
name: str
age: int = 0
u = User(name="ada", age="forty") # both tools flag thisPick ty here if you have a lot of Pydantic models and prefer one tool-level default over per-model ConfigDict lines. Otherwise stay on Pyrefly and set strict=True on the models that need it.
Diverge on code with no annotations
The two tools take opposite stances when a value has no annotation. Pyrefly infers concrete types from the surrounding code and uses what it learns to flag wrong-type usage. ty deliberately declines to infer concrete types when the only evidence is implicit; it labels the gap as Unknown and lets the call through.
The same snippet produces opposite verdicts (both tools at their normal checking level, Pyrefly with preset = "default"):
x = []
x.append(1)
x.append("foo")- ty:
All checks passed!(typesxaslist[Unknown]because the literal is empty) - Pyrefly:
bad-argument-type(inferslist[int]from the first append, rejects the second)
The same split fires on unannotated function returns and class attributes assigned None. With a non-empty literal (my_list = [1, 2, 3]) both tools agree, because the literal is direct evidence neither needs to guess at.
For most readers this does not change the recommendation. Annotated code behaves identically across both. The split matters mostly for codebases with large amounts of unannotated legacy code: Pyrefly will surface more errors out of the gate, ty will stay quieter and ask for annotations to opt in to stricter checking. This is independent of the basic preset trap, which silences additional errors via configuration regardless of annotation density.
Watch out for Pyrefly’s basic preset
The most common Pyrefly mistake is running it without pyrefly init and assuming the green output means clean code. With no pyrefly.toml in the project, Pyrefly applies the basic preset, which only flags syntax errors, missing imports, and unknown names. Wrong-argument types, missing returns, and most of what users expect a type checker to catch are silenced.
The naming is confusing: there is also a preset literally named default, but it only applies once a config file exists and no preset = line is set. A project with no config file gets basic, not default. The Pyrefly configuration docs state the rule directly: “Otherwise Pyrefly uses the same basic preset the IDE uses.”
The five presets, from least to most strict:
offsilences every error kind. For projects opting in to specific checks manually.basicis the unconfigured-project default. Low-noise, high-confidence diagnostics only.legacydisables three Pyrefly checks mypy doesn’t implement. Emitted bypyrefly initwhen it detects a mypy config.defaultis the standard Pyrefly experience. Use this for new projects.strictaddsstrict-callable-subtyping,implicit-any,missing-override-decorator, andunused-ignoreon top ofdefault. For codebases that wantAnykept out.
Run pyrefly init and choose default or strict. Or set preset = "default" in pyrefly.toml (or [tool.pyrefly] in pyproject.toml) by hand. The full preset reference is in the Pyrefly Reference page.
ty has no equivalent preset system. Diagnostics are configured by setting individual rules under [tool.ty.rules].
What both still miss
Neither tool has a mypy-style plugin system that user code can extend. That leaves a few surfaces unchecked under both:
- SQLAlchemy 2.0 constructor arguments. Both tools read
Mapped[T]field types correctly, sou.idreveals asint. But the declarativeBase.__init__uses**kwargs, and neither tool checksUser(id="not-an-int")against the field annotations. - Celery task signatures. The mypy plugin for Celery validated
.delay()and.apply_async()calls against the task’s declared parameters. No equivalent in ty or Pyrefly. - Older
@attr.ssyntax.@attrs.defineships PEP 681 metadata and gets checked. Pre-PEP-681 attrs classes don’t, in either tool. - Arbitrary user plugins. If your codebase ran on a custom mypy plugin (a builder pattern, a metaclass-heavy API), there’s nowhere to plug equivalent logic into ty or Pyrefly today.
Migrating from mypy plus plugins is the case where both tools are a step back from mypy. Without plugins, both are a step forward.
Learn More
- Pyrefly reference
- ty reference
- How do Python type checkers compare? covers mypy, pyright, Basedpyright, and Zuban as well
- How to migrate from mypy to Pyrefly
- How to migrate from mypy to ty
- How to add type annotations with pyrefly infer
- How to gradually adopt type checking in an existing Python project
- Pyrefly 1.0 release notes
- Python typing conformance dashboard shows live numbers