# ty vs Pyrefly: which Python type checker should you pick?


[ty](https://pydevtools.com/handbook/reference/ty.md) (Astral) and [Pyrefly](https://pydevtools.com/handbook/reference/pyrefly.md) (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](https://pydevtools.com/handbook/reference/mypy.md), [pyright](https://pydevtools.com/handbook/reference/pyright.md), [Basedpyright](https://pydevtools.com/handbook/reference/basedpyright.md), and [Zuban](https://pydevtools.com/handbook/reference/zuban.md), see [How do Python type checkers compare?](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md).

## 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](#watch-out-for-pyreflys-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](https://pyrefly.org/blog/v1.0/) 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](https://pydevtools.com/handbook/reference/ty.md) 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](https://github.com/python/typing/tree/main/conformance), 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](https://htmlpreview.github.io/?https://github.com/python/typing/blob/main/conformance/results/results.html) 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](https://github.com/astral-sh/ty/issues/1889).

### Pyrefly translates mypy configs with one command

If you're migrating from [mypy](https://pydevtools.com/handbook/reference/mypy.md), 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](https://pydevtools.com/handbook/how-to/how-to-add-type-annotations-with-pyrefly-infer.md).
- `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](https://pydevtools.com/handbook/how-to/how-to-migrate-from-mypy-to-ty.md) is a manual translation. Astral has signaled an automatic option is on the roadmap; it isn't there yet.

### Pyrefly understands Pydantic and Django out of the box

[Pyrefly 1.0]({{< relref "handbook/Reference/pyrefly.md#key-features" >}}) ships built-in support for both frameworks: field types, autocomplete on model fields, and validation modeling. Django is the clearest win. 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's dataclass_transform marker](https://pydevtools.com/handbook/explanation/what-is-pep-681.md) generically, which works because Pydantic's `BaseModel` carries it. Django models don't, 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. Pyrefly models Pydantic too, but it follows Pydantic's lax-by-default validation rather than the annotation, which is the one place ty checks more strictly. [When ty is the better pick](#when-ty-is-the-better-pick) covers that trade-off.

## When ty is the better pick

ty applies PEP 681's `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:

```python
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:

```python
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 this
```

Pick 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"`):

```python
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](#watch-out-for-pyreflys-basic-preset), 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](https://pyrefly.org/en/docs/configuration/) 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]({{< relref "handbook/Reference/pyrefly.md#configuration-presets" >}}).

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

- [Pyrefly](https://pydevtools.com/handbook/reference/pyrefly.md) reference
- [ty](https://pydevtools.com/handbook/reference/ty.md) reference
- [How do Python type checkers compare?](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md) covers mypy, pyright, Basedpyright, and Zuban as well
- [How to migrate from mypy to Pyrefly](https://pydevtools.com/handbook/how-to/how-to-migrate-from-mypy-to-pyrefly.md)
- [How to migrate from mypy to ty](https://pydevtools.com/handbook/how-to/how-to-migrate-from-mypy-to-ty.md)
- [How to add type annotations with pyrefly infer](https://pydevtools.com/handbook/how-to/how-to-add-type-annotations-with-pyrefly-infer.md)
- [How to gradually adopt type checking in an existing Python project](https://pydevtools.com/handbook/how-to/how-to-gradually-adopt-type-checking-in-an-existing-python-project.md)
- [Pyrefly 1.0 release notes](https://pyrefly.org/blog/v1.0/)
- [Python typing conformance dashboard](https://htmlpreview.github.io/?https://github.com/python/typing/blob/main/conformance/results/results.html) shows live numbers
