# Migrating from mypy to ty: Lessons from FastAPI

Sebastián Ramírez, creator of [FastAPI](https://github.com/tiangolo/fastapi), [recently announced](https://x.com/tiangolo/status/1907849737048363046) that all his Python projects now use [ty](https://pydevtools.com/handbook/reference/ty.md) for type checking. That includes FastAPI, [Typer](https://github.com/fastapi/typer), [SQLModel](https://github.com/fastapi/sqlmodel), [Asyncer](https://github.com/fastapi/asyncer), and [FastAPI CLI](https://github.com/fastapi/fastapi-cli).

Migrating the projects wasn't a hard switch. It was incremental, messy in the middle, and completed at different speeds across different projects. That pattern offers a useful template for anyone considering the same move.

Ramírez didn't rip out [mypy](https://pydevtools.com/handbook/reference/mypy.md) and replace it with ty in a single PR. Instead, he added ty alongside mypy and ran both checkers in parallel. Four of his five major repos still operate this way. Only SQLModel has fully dropped mypy.

The configuration is minimal. The only ty-specific setting used across these projects is:

```toml
[tool.ty.terminal]
error-on-warning = true
```

[This](https://docs.astral.sh/ty/reference/configuration/#error-on-warning) makes ty exit non-zero on warnings, preventing them from accumulating silently. FastAPI omits even this, likely because it was the first repo to adopt ty (March 2026) before the pattern was established.

## The ugly middle: dual ignore comments

Running [two type checkers](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md) simultaneously creates a predictable problem. mypy and ty disagree about certain constructs. When mypy flags something that ty considers fine, you end up with lines like:

```python
func  # type: ignore[assignment]  # ty: ignore[unused-ignore-comment]
```

The first comment silences mypy. The second silences ty's complaint that the first comment is [unnecessary](https://docs.astral.sh/ty/reference/rules/#unused-ignore-comment). This pattern appears throughout FastAPI's codebase: in `applications.py`, `routing.py`, `params.py`, and `dependencies/utils.py`, among others.

> [!TIP]
> A [recent ty change](https://github.com/astral-sh/ruff/pull/24096), available in ty >= 0.0.25, makes this less painful. ty now supports `type: ignore[ty:code]` syntax, so you can suppress errors from both checkers in a single comment: `# type: ignore[assignment, ty:unused-ignore-comment]`. Since mypy ignores unknown rule codes, this works with both checkers without the double-comment workaround.

The [FastAPI adoption PR](https://github.com/tiangolo/fastapi/pull/15091) acknowledged the tradeoff directly: the double-ignore comments are ugly, but they can be removed when mypy is dropped.

This is the cost of a gradual migration. The alternative is a flag day where you switch everything at once, but on a codebase the size of FastAPI (which had roughly 150 ty errors to resolve on initial adoption), that's a harder sell.

## SQLModel: the full cutover

SQLModel was the first project to [drop mypy entirely](https://github.com/tiangolo/sqlmodel/pull/1806). The rationale was practical: the volume of `type: ignore` comments that ty considered unnecessary made the dual-checker approach worse than committing to ty alone.

After the cutover, SQLModel uses [ty-specific ignore codes](https://docs.astral.sh/ty/suppressing-errors/) where needed:

```python
# ty: ignore[subclass-of-final-class]
# ty: ignore[invalid-method-override]
# ty: ignore[invalid-argument-type]
```

The `sqlmodel/sql/expression.py` file alone has roughly 15 `ty: ignore[invalid-argument-type]` comments, mostly for SQLAlchemy API calls where ty's type inference is stricter than mypy's. These represent genuine disagreements between the checker and the library's type stubs, not bugs in the application code.

## How ty fits into the workflow

Across all five projects, ty runs in two places:

Lint scripts: Each repo has a `scripts/lint.sh` that runs ty alongside [ruff](https://pydevtools.com/handbook/reference/ruff.md):

```bash
ty check package_name
ruff check ...
ruff format ... --check
```

Pre-commit hooks: All repos use local [pre-commit](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md) hooks that invoke ty through [uv](https://pydevtools.com/handbook/reference/uv.md):

```yaml
- id: local-ty
  name: ty check
  entry: uv run ty check package_name
  require_serial: true
  language: unsupported
  pass_filenames: false
```

The `pass_filenames: false` setting means ty always checks the full package, not just changed files.

In CI, these hooks run via [prek](https://pydevtools.com/handbook/reference/prek.md) rather than as standalone workflow steps.

## What this tells us about ty's readiness

Ramírez's adoption is notable because FastAPI is one of the most widely used Python frameworks. The fact that ty works well enough for these projects, even alongside mypy during transition, signals that ty has crossed a usability threshold for well-typed codebases.

A few caveats apply. These are library projects with strong existing type annotations. They don't use [mypy plugins](https://mypy.readthedocs.io/en/stable/extending_mypy.html) (no Pydantic mypy plugin, no Django stubs). Projects that depend on mypy's plugin ecosystem face a harder migration path, because ty has no plugin system and [no plans to add one](https://docs.astral.sh/ty/).

The rapid version churn also tells a story: Dependabot PRs bumping ty appear regularly across all five repos, tracking versions from 0.0.23 through 0.0.28. This is a tool that's still changing fast.

## Applying this to your own projects

If you're considering a similar migration, Ramírez's approach suggests a template:

1. Add ty alongside mypy. Don't remove anything yet. Run `ty check` in your lint script or [CI](https://pydevtools.com/handbook/how-to/how-to-use-ty-in-ci.md) and see what surfaces.
2. Set `error-on-warning = true`. This is the one piece of configuration every project settled on. It prevents warning creep.
3. Accept the double-ignore comments. They're ugly but temporary. The alternative is waiting until you're ready for a complete switch, which may never happen.
4. Pick a smaller project to cut over first. SQLModel went first, not FastAPI. Start where the risk is lowest.
5. Drop mypy when the noise exceeds the signal. The trigger for SQLModel's full cutover was that the `type: ignore` maintenance burden outweighed the benefit of keeping mypy around.

For a detailed walkthrough of the migration mechanics, including configuration mapping, error code equivalents, and CI setup, see our guide on [how to migrate from mypy to ty](https://pydevtools.com/handbook/how-to/how-to-migrate-from-mypy-to-ty.md).
