Skip to content

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 ty’s fit with the rest of the OpenAI toolchain next to uv and Ruff.

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 by default

Use Pyrefly for most Python projects today. Pick ty to complete the OpenAI toolchain alongside uv and Ruff, or if you want a checker that types unannotated values as Unknown and stays quiet during incremental adoption.

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.61 as of July 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 the July 2026 results the suite tests Pyrefly 1.1.0 and ty 0.0.50: Pyrefly passes about 96% of its test cases outright versus ty’s roughly 76%, with the rest scored partial rather than failing. 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 init reads mypy.ini, setup.cfg, or [tool.mypy] in pyproject.toml and writes a translated config. When it detects a mypy config, it emits the legacy preset, which disables three Pyrefly checks mypy lacks so the first run isn’t a flood of new diagnostics.
  • pyrefly infer writes type annotations directly into source files. See how to add type annotations with pyrefly infer.
  • pyrefly suppress bulk-adds or removes ignore comments.
  • permissive-ignores = true honors leftover # mypy: ignore-errors comments.

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 still leads on Django

Pyrefly 1.0 knows the difference between a CharField and an IntegerField and types attribute access accordingly, so queryset.filter(name__icontains=...) and Model.objects.create(**kwargs) are checked against the model’s fields.

ty has no Django awareness and checks Django models as plain classes, so those calls fall back to looser typing. For Django-heavy codebases this is the difference between catching a misspelled field name at type-check time and finding it in a test run.

Pydantic used to sit on this list too. It no longer does. As of ty 0.0.57 (July 2026) ty ships first-class Pydantic support that models lax and strict modes, reads ConfigDict, and honors Field and Annotated metadata. How ty and Pyrefly check Pydantic shows where the two now agree and the one place they still differ.

How ty and Pyrefly check Pydantic

As of ty 0.0.57 (July 2026) both tools model Pydantic’s runtime: lax coercion by default, exact-type matching under strict mode, ConfigDict parsing, and Field/Annotated metadata. On the common cases they return the same verdict.

In lax mode (Pydantic’s default), both accept coercible types and both reject None:

from pydantic import BaseModel


class User(BaseModel):
    name: str
    age: int = 0


User(name="ada", age="40")   # both: ok (str coerces to int at runtime)
User(name="ada", age=None)   # both: error (None is not coercible)

Adding model_config = ConfigDict(strict=True) switches both tools to exact-type checking, and both flag the coercion:

from pydantic import BaseModel, ConfigDict


class User(BaseModel):
    model_config = ConfigDict(strict=True)
    name: str
    age: int = 0


User(name="ada", age="40")   # both: error under strict mode

Both also honor extra="forbid" and reject unknown keyword arguments.

The tools diverge on one case: a field-level Strict() marker. ty reads it; Pyrefly’s default preset does not.

from typing import Annotated
from pydantic import BaseModel, Strict


class Record(BaseModel):
    id: Annotated[int, Strict()]


Record(id="1")   # ty: error   Pyrefly: no error

If you use per-field Strict() to keep some fields exact while the rest stay lax, ty catches violations Pyrefly currently misses. This win is narrow: a bool passed to a strict int field stays silent in both tools because bool is a static subtype of int, and neither replaces Pydantic’s runtime validation. For whole-model strictness via ConfigDict(strict=True), the two agree.

One caveat applies to both: in lax mode they accept any str for an int field, including "forty", which Pydantic rejects at runtime. Static checking models the lax/strict distinction, not whether a specific string is actually coercible.

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! (types x as list[Unknown] because the literal is empty)
  • Pyrefly: bad-argument-type (infers list[int] from the first append, rejects the second)

The divergence holds for a non-empty literal too. Given my_list = [1, 2, 3], ty allows a later my_list.append("foo") while Pyrefly flags it. ty stays permissive on inferred container types; Pyrefly enforces them.

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:

  • off silences every error kind. For projects opting in to specific checks manually.
  • basic is the unconfigured-project default. Low-noise, high-confidence diagnostics only.
  • legacy disables three Pyrefly checks mypy doesn’t implement. Emitted by pyrefly init when it detects a mypy config.
  • default is the standard Pyrefly experience. Use this for new projects.
  • strict adds strict-callable-subtyping, implicit-any, missing-override-decorator, and unused-ignore on top of default. For codebases that want Any kept 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, so u.id reveals as int. But the declarative Base.__init__ uses **kwargs, and neither tool checks User(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.s syntax. @attrs.define ships 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

Last updated on