Skip to content

How to gradually adopt type checking in an existing Python project

This guide assumes you have a Python project managed by uv. If you haven’t created a project yet, see the project creation tutorial before proceeding.

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 mypy

Run 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 = true

This 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 = true

Add 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 dict and list with dict[str, int] or list[str]
  • Add from __future__ import annotations to use newer annotation syntax on older Python versions
  • Install type stubs for third-party libraries (uv add --dev types-requests for the requests package, 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.

Learn More

Last updated on

Please submit corrections and feedback...