How to migrate from mypy to ty
ty is a fast Python type checker from Astral, the uv and Ruff team now part of OpenAI. It checks large codebases in a fraction of the time mypy takes. This guide answers whether switching is worth it today, then walks through translating a mypy setup to ty.
Should you switch from mypy to ty?
Not yet, for most projects. ty is in beta (0.0.56 as of July 2026), with a stable 1.0 targeted for 2026. It can change behavior between pre-1.0 releases, and it has no plugin system, so codebases that depend on pydantic.mypy, django-stubs, or sqlalchemy[mypy] should stay on mypy for now.
Switch to ty if your project fits this profile:
- Type checking is slow enough on a large codebase that speed matters. ty runs roughly 9x faster than single-threaded mypy on typical code.
- You want ty’s gradual guarantee: adding annotations to working code narrows existing errors instead of surfacing new ones, which makes incremental adoption predictable.
- You already run the OpenAI toolchain (uv, Ruff) and want ty to match.
- You don’t depend on mypy plugins.
Wait on ty if any of these hold:
- You rely on
pydantic.mypy,django-stubs, orsqlalchemy[mypy]. - You need the stability of a checker that has shipped since 2012.
- A breaking change between beta releases would disrupt your CI.
Important
If you are leaving mypy and don’t specifically need ty’s gradual guarantee or OpenAI-toolchain fit, weigh Pyrefly too. The handbook recommends Pyrefly (Meta) as the default type checker for new projects: it reached a stable 1.0 in May 2026, ships built-in Pydantic and Django support, and passes more of the typing conformance suite than ty. See ty vs Pyrefly for the head-to-head and How do Python type checkers compare? for the full field.
Install ty and run your first check
Install ty
ty is distributed as a standalone binary. The easiest way to use it is via uvx:
# Run ty directly without installation
uvx ty check .
# Or install globally with uv
uv tool install ty
# Then run
ty check .For other installation methods, see the ty installation guide.
Map mypy commands to ty
| mypy Command | ty Equivalent |
|---|---|
mypy . |
ty check . |
mypy src/ |
ty check src/ |
mypy -p mypackage |
ty check path/to/mypackage/ |
mypy --python-version 3.11 . |
ty check . --python-version 3.11 |
mypy --strict . |
No direct equivalent (see configuration) |
For all available CLI options, see the ty CLI reference.
-p; pass file or directory paths instead, or use --project or environment.root to define source roots.Point ty at your Python environment
ty needs to find installed packages to resolve imports:
# Point to a virtualenv or interpreter
ty check . --python .venv
# Or let ty auto-detect from an active virtualenv
ty check .For more details on how ty discovers and resolves modules, see the module discovery documentation.
Choose an output format
# Default verbose output
ty check .
# Concise one-line-per-error output
ty check . --output-format concise
# GitHub Actions annotations
ty check . --output-format github
# GitLab Code Quality format
ty check . --output-format gitlabSuppress existing errors with –add-ignore
When migrating from mypy to ty, you may encounter many new type errors that you want to address gradually. The --add-ignore flag automates the process of suppressing these errors by adding ty: ignore[codes] comments throughout your codebase.
# Add ignore comments for all current errors
ty check . --add-ignore
# Verify no new errors remain
ty check .ty groups multiple error codes per comment, extends existing ty: ignore comments where they already exist, and validates that the file still parses. Treat the suppressed errors as a todo list, not a fix.
Translate your mypy configuration
Find where ty reads config
mypy supports:
mypy.inipyproject.toml→[tool.mypy]setup.cfg→[mypy].mypy.ini
ty supports:
ty.toml(takes precedence)pyproject.toml→[tool.ty]- User-level:
~/.config/ty/ty.toml
For complete configuration details, see the ty configuration guide.
Map mypy options to ty
| mypy Option | ty Equivalent | Notes |
|---|---|---|
python_version = "3.11" |
environment.python-version = "3.11" |
|
ignore_missing_imports = true |
rules.unresolved-import = "ignore" |
Critical for most projects |
exclude = ["tests/"] |
src.exclude = ["**/tests/**"] |
Uses glob patterns |
follow_imports = "silent" |
No equivalent | ty doesn’t have import following modes |
check_untyped_defs = true |
Default behavior | ty checks all code by default |
disallow_untyped_defs = true |
See note below | Enforce via Ruff ANN001/ANN201 |
strict = true |
No equivalent | Must enable individual options |
plugins = ["pydantic.mypy"] |
Not supported | Major migration blocker |
If you relied on disallow_untyped_defs = true, you can approximate it with Ruff’s flake8-annotations rules:
[tool.ruff.lint]
select = [
"ANN001", # flake8-annotations
"ANN201",
]See Ruff rule docs for missing-type-function-argument and missing-return-type-undocumented-public-function.
Example configuration migration
mypy (pyproject.toml):
[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true
exclude = ["tests/", "docs/"]
check_untyped_defs = true
warn_redundant_casts = truety (pyproject.toml):
[tool.ty.environment]
python-version = "3.11"
[tool.ty.terminal]
error-on-warning = true
[tool.ty.src]
exclude = ["**/tests/**", "**/docs/**"]
[tool.ty.rules]
unresolved-import = "ignore"
# Note: check_untyped_defs is ty's default behavior
# warn_redundant_casts equivalent: warnings are on by defaultTip
error-on-warning = true makes ty exit non-zero on warnings, not just errors. This prevents warnings from accumulating silently and is recommended for any CI or pre-commit setup. Sebastián Ramírez uses this setting across all his projects including FastAPI, Typer, and SQLModel.
ty.toml, drop the tool.ty prefix (e.g., [environment], [src], [rules]).Set up per-module overrides
mypy:
[[tool.mypy.overrides]]
module = ["mypackage.legacy", "mypackage.legacy.*"]
ignore_errors = true
disallow_untyped_defs = falsety:
[[tool.ty.overrides]]
include = ["mypackage/legacy/**"]
rules = { unresolved-attribute = "ignore", invalid-assignment = "ignore" }Handle mypy plugins
ty has no plugin system and no plan to add one. Some popular libraries may get native ty support over time.
If your project uses mypy plugins, this is a significant migration consideration.
| Plugin | ty Support | Workaround |
|---|---|---|
pydantic.mypy |
❌ Not supported | Ignore Pydantic-related rules |
django-stubs |
❌ Not supported | Wait for native support or use mypy |
sqlalchemy[mypy] |
❌ Not supported | Wait for native support or use mypy |
numpy.mypy |
❌ Not supported | May work with stubs |
Workaround for Pydantic projects:
[tool.ty.rules]
invalid-argument-type = "ignore" # Pydantic model constructors
invalid-key = "ignore" # TypedDict/Pydantic interop
invalid-parameter-default = "ignore" # Field defaultsRun ty in CI
Map your current mypy CI setup
Common patterns found in production projects:
Pattern 1: Dedicated mypy workflow
name: Mypy
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -e .[dev]
- run: mypy src/Pattern 2: Matrix across Python versions
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- run: mypy google/genai/Migrating to ty
Replace mypy with ty:
name: Type Check
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -e .[dev]
- run: pip install uv
# Use uvx to run ty without installation
- run: uvx ty check src/ --python .venv
# Or, if ty is a dev dependency:
# - run: uv run ty check src/With GitHub Actions annotations:
- run: uvx ty check src/ --output-format githubRunning both type checkers during transition
During migration, run both checkers:
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -e .[dev] mypy
- run: mypy src/
ty:
runs-on: ubuntu-latest
continue-on-error: true # Don't fail build yet
steps:
- uses: actions/checkout@v4
- run: pip install -e .[dev]
- run: uvx ty check src/ --python .venv || trueHandling dual-ignore comments
When running both checkers, you will encounter cases where mypy requires a # type: ignore comment that ty considers unnecessary. ty flags these with unused-ignore-comment, producing lines like:
result = func() # type: ignore[assignment] # ty: ignore[unused-ignore-comment]The first comment silences mypy. The second silences ty’s complaint about the first. This is an unavoidable cost of the dual-checker approach and a strong signal that the codebase is ready to drop mypy.
Resolve ty’s extra errors
To decode any unfamiliar ty error code, run ty explain rule <code> (for example, ty explain rule unresolved-import) to print its description, default severity, and rationale.
Issue 1: ty checks untyped code by default
Symptom: Increase in errors for unannotated codebases.
Why it happens: mypy skips function bodies without type annotations by default. ty checks everything.
Example (NetworkX: 580 files, ~192k LOC):
- mypy: 0 errors
- ty: ~1,200 errors
Solution for unannotated code:
[tool.ty.rules]
# Ignore errors that arise from missing type info
unresolved-attribute = "ignore"
possibly-missing-attribute = "ignore"
invalid-assignment = "ignore"Or exclude unannotated modules (see file exclusions):
[tool.ty.src]
exclude = ["legacy/**", "untyped/**"]Issue 2: Missing import resolution
Symptom: unresolved-import errors for installed packages.
Why it happens: ty needs to know where the Python environment is.
Solution:
[tool.ty.environment]
python = ".venv"Or via CLI:
uvx ty check . --python .venvFor optional dependencies:
[tool.ty.rules]
unresolved-import = "ignore"Issue 3: Different type narrowing behavior
Symptom: Errors like “Attribute may be missing” after type guards.
Example (from IPython):
if _is_sizable(completions):
return len(completions) != 0 # ty: Expected Sized, found IteratorWhy it happens: ty and mypy have different type narrowing algorithms.
Solution: Usually requires code changes or ignoring specific rules:
[tool.ty.rules]
possibly-missing-attribute = "ignore"Or inline comments:
return len(completions) != 0 # ty: ignore[invalid-argument-type]Issue 4: Pydantic model errors
Symptom: Many errors related to Pydantic BaseModel, Field, TypedDict.
Example (from python-genai):
options: RequestOptions = {} # ty error: dict not assignable to RequestOptionsWhy it happens: No Pydantic plugin support in ty.
Solution: Extensive rule ignoring:
[tool.ty.rules]
invalid-argument-type = "ignore"
invalid-key = "ignore"
invalid-parameter-default = "ignore"Issue 5: No equivalent for ignore_errors = true
Symptom: Modules with ignore_errors = true in mypy still report errors.
Solution: Use overrides to ignore all rules for those files:
[[tool.ty.overrides]]
include = ["src/legacy/**"]
[tool.ty.overrides.rules]
unresolved-attribute = "ignore"
invalid-assignment = "ignore"
invalid-argument-type = "ignore"Or exclude them entirely:
[tool.ty.src]
exclude = ["src/legacy/**"]Map mypy error codes to ty rules
This table maps common mypy error codes to their ty rule equivalents.
| mypy Code | ty Rule | Description |
|---|---|---|
| import-not-found | unresolved-import | Module not found |
| attr-defined | unresolved-attribute | Unknown attribute |
| arg-type | invalid-argument-type | Wrong argument type |
| assignment | invalid-assignment | Wrong assignment type |
| return-type | invalid-return-type | Wrong return type |
| call-arg | invalid-argument-type | Wrong call arguments |
| index | non-subscriptable | Invalid subscript |
| union-attr | possibly-missing-attribute | Attribute may be missing |
| override | invalid-method-override | Invalid method override |
| redundant-cast | redundant-cast | Unnecessary cast |
| unused-ignore | unused-ignore-comment | Unused type: ignore / ty: ignore |
| no-redef | conflicting-declarations | Name redefinition |
| valid-type | invalid-type-form | Invalid type expression |