How to migrate from mypy to ty
ty, built by Astral, now part of OpenAI, uses different configuration and diagnostics from mypy. Run both against the same files before replacing mypy in CI. Projects that depend on mypy plugins need to verify the corresponding framework behavior in ty first.
Install ty alongside mypy
In a project managed by uv:
uv add --dev ty
uv run ty check src/The --dev flag marks ty as a development dependencyA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag.
, keeping it out of production installs. uv run executes the command inside the project’s virtual environment, ensuring all dependencies are installed first.
A clean check prints All checks passed!. Otherwise, ty reports diagnostics with rule names such as invalid-assignment. Keep mypy installed while reviewing the differences.
| mypy command | ty command |
|---|---|
mypy src/ |
uv run ty check src/ |
mypy --python-version 3.12 src/ |
uv run ty check src/ --python-version 3.12 |
mypy -p mypackage |
uv run ty check path/to/mypackage/ |
mypy --strict src/ |
Configure individual ty rules and annotation linting |
Translate configuration by intent
mypy discovers mypy.ini, .mypy.ini, [tool.mypy] in pyproject.toml, or [mypy] in setup.cfg. ty reads ty.toml or [tool.ty] in pyproject.toml, searching from the working directory toward its parents. ty.toml wins when both exist in the same directory.
User configuration is merged with project settings. It lives under ~/.config/ty/ty.toml on macOS/Linux or %APPDATA%\ty\ty.toml on Windows. Keep CI-relevant settings in the repository so local user settings are not required.
| mypy setting | ty setting or migration step |
|---|---|
python_version = "3.12" |
environment.python-version = "3.12" |
exclude = ["tests/"] |
src.exclude = ["tests/"]; translate regular expressions into glob patterns |
ignore_missing_imports = true |
Prefer package-specific analysis.allowed-unresolved-imports over disabling all import diagnostics |
check_untyped_defs = true |
ty checks unannotated function bodies by default |
disallow_untyped_defs = true |
Enforce annotations separately with Ruff’s ANN rules |
warn_redundant_casts = true |
rules.redundant-cast = "warn" (already the default) |
strict = true |
No equivalent preset; compare ty’s migration guidance with the checks the project needs |
plugins = [...] |
No plugin loading; test native framework support or keep mypy |
For a src/ layout with a known untyped dependency, start with:
[tool.ty.environment]
python-version = "3.12"
[tool.ty.src]
include = ["src"]
exclude = ["tests/"]
[tool.ty.analysis]
allowed-unresolved-imports = ["some_untyped_library.**"]
[tool.ty.rules]
redundant-cast = "warn"Replace some_untyped_library with the dependency the project intentionally permits. This suppresses matching unresolved-import diagnostics; it does not reproduce every effect of mypy’s import-following settings. ty normally discovers the project’s .venv; use --python .venv only when environment discovery needs an explicit path.
In ty.toml, omit the tool.ty prefix. Explicit paths bypass src.exclude unless --force-exclude is set.
Compare unannotated code and framework behavior
mypy skips completely unannotated function bodies by default. ty checks operations inside those bodies, but unannotated parameters still have unknown types. Body checking does not imply inference of a parameter’s type from every call site.
Unannotated return types are Unknown by default in ty. To infer supported return types from function bodies, opt in:
[tool.ty.analysis]
return-type-inference = "always"This changes the types seen by callers. Review the resulting diagnostics before enabling it in CI. See how checkers handle untyped code.
For plugin-dependent projects, compare representative model constructors, ORM queries, and generated attributes. Native support for a library does not guarantee parity with its mypy plugin. Keep mypy if a required check disappears during the migration.
Scope legacy exceptions
mypy overrides match module names; ty overrides match file paths. To disable checks temporarily for a legacy directory while retaining its inferred types for imports:
[[tool.ty.overrides]]
include = ["src/mypackage/legacy/**"]
rules = { all = "ignore" }Use this only for an explicit backlog. A short list of disabled rules does not mean “ignore all errors,” and downgrading a rule to warn still fails ty’s default CI exit policy.
Review suppression comments
A bare # type: ignore suppresses ty diagnostics on that line. A mypy code such as # type: ignore[assignment] does not suppress ty’s invalid-assignment. Use separate targeted comments when both checkers need an exception:
value: int = "legacy" # type: ignore[assignment] # ty: ignore[invalid-assignment]This example intentionally retains an invalid assignment as migration debt. ty warns about unused ty-specific suppressions by default. Existing mypy-specific comments do not require an extra suppression merely because both tools run.
For a larger backlog, ty can insert targeted comments:
uv run ty check src/ --add-ignore
uv run ty check src/The first command edits source files. Review that diff; the second should print All checks passed! if all remaining diagnostics were suppressed. Suppression records debt rather than correcting the code.
Run both checkers before switching CI
Keep the current mypy step and add ty after synchronizing the committed lockfile:
- run: uv sync --locked
- run: uv run --locked mypy src/
- name: Evaluate ty
continue-on-error: true
run: uv run --locked ty check src/ --output-format githubThe advisory ty step still displays a failed check, so new diagnostics stay visible. Remove continue-on-error once the reviewed configuration and suppressions give a clean result. See the complete ty CI recipe for installation and report setup.
ty exits 0 for a clean check and 1 for warning or error diagnostics by default. --exit-zero-on-warning allows warnings while retaining failures for errors; fatal setup errors or invalid CLI usage use 2.
Remove mypy after validating coverage
Remove mypy’s CI step and development dependency once ty checks the intended files and framework behavior. Retain stub packages that ty still needs; a types-* dependency is not necessarily mypy-specific.
uv remove --dev mypyuv updates pyproject.toml and uv.lock. Delete the old mypy configuration only after confirming that no other workflow still uses it.