Skip to content

Migrating from mypy to ty: Lessons from FastAPI

April 3, 2026·Tim Hopper

Sebastián Ramírez, creator of FastAPI, recently announced that all his Python projects now use ty for type checking. That includes FastAPI, Typer, SQLModel, Asyncer, and 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 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:

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

This 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 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:

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. 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, 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 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. 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 where needed:

# 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:

ty check package_name
ruff check ...
ruff format ... --check

Pre-commit hooks: All repos use local pre-commit hooks that invoke ty through uv:

- 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 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 (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.

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 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.

Last updated on

Please submit corrections and feedback...