How to gradually adopt type checking in an existing Python project
Running a type checker on an established codebase for the first time produces hundreds or thousands of errors. That wall of red discourages adoption before it starts. The practical path is to check new code strictly while ignoring legacy issues, then shrink the ignore list over time.
This guide uses mypy, but the same strategy applies to pyright and ty.
Add mypy to the project
uv add --dev mypyRun mypy and capture the baseline
Run mypy against the full codebase to see what you’re working with:
uv run mypy .The output will likely contain errors across many files. That’s expected.
Suppress existing errors per-module
Rather than fixing everything upfront, configure mypy to ignore errors in modules that haven’t been typed yet. Add a section to pyproject.toml that silences errors in legacy modules:
[tool.mypy]
python_version = "3.12"
strict = false
# Enforce type checking on new or migrated modules
[[tool.mypy.overrides]]
module = ["mypackage.api.*", "mypackage.models.*"]
disallow_untyped_defs = true
warn_return_any = true
# Silence errors in legacy modules for now
[[tool.mypy.overrides]]
module = ["mypackage.legacy.*", "mypackage.utils.*"]
ignore_errors = trueThis approach has two lists: modules that are checked strictly and modules that are ignored. As code gets typed, move modules from the second list to the first.
Start strict on new code
Set a rule for the team: all new files and modules must pass type checking. Enforce this by adding new module paths to the strict overrides section as they’re created.
A useful pattern is to invert the approach once most code is typed. Instead of listing which modules to check, list which modules to skip:
[tool.mypy]
strict = true
# Only these legacy modules are still exempt
[[tool.mypy.overrides]]
module = ["mypackage.legacy_parser", "mypackage.old_cli"]
ignore_errors = trueAdd type checking to CI
Add mypy to the test suite so type errors block merges:
uv run mypy .This works alongside uv run pytest and uv run ruff check . in a CI pipeline. See Setting up GitHub Actions with uv for CI configuration details.
Chip away at untyped modules
Pick one module at a time. Remove it from the ignore list, fix the type errors, and commit. Smaller modules go faster and build momentum.
Common fixes when typing a module for the first time:
- Add return type annotations to functions
- Replace bare
dictandlistwithdict[str, int]orlist[str] - Add
from __future__ import annotationsto use newer annotation syntax on older Python versions - Install type stubs for third-party libraries (
uv add --dev types-requestsfor therequestspackage, for example)
Use per-file ignores for stubborn errors
Some errors in a mostly-typed module aren’t worth fixing immediately. Use inline comments to suppress individual lines:
result = some_untyped_library.process(data) # type: ignore[no-untyped-call]Always include the error code in brackets. Bare # type: ignore comments mask real issues that appear later.