# How to gradually adopt type checking in an existing Python project

{{< callout type="warning" >}}
This guide assumes you have a Python project managed by [uv](https://pydevtools.com/handbook/reference/uv.md). If you haven't created a project yet, see the [project creation tutorial](https://pydevtools.com/handbook/tutorial/create-your-first-python-project.md) before proceeding.
{{< /callout >}}

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](https://pydevtools.com/handbook/reference/mypy.md), but the same strategy applies to [pyright](https://pydevtools.com/handbook/reference/pyright.md) and [ty](https://pydevtools.com/handbook/reference/ty.md).

## Add mypy to the project

```bash
uv add --dev mypy
```

## Run mypy and capture the baseline

Run mypy against the full codebase to see what you're working with:

```bash
uv run mypy .
```

The output will likely contain errors across many files. That's expected.

## Bootstrap annotations with a codemod (optional)

On a large unannotated codebase, manually adding return types and parameter annotations to every function is the slow path. A codemod that writes annotations from static analysis or runtime traces can compress weeks of work into a reviewable diff.

[`pyrefly infer`](https://pydevtools.com/handbook/how-to/how-to-add-type-annotations-with-pyrefly-infer.md) is the static option: it reads call sites and writes annotations directly into source files, no runtime instrumentation required. It works whether you ultimately use [Pyrefly](https://pydevtools.com/handbook/reference/pyrefly.md), mypy, [ty](https://pydevtools.com/handbook/reference/ty.md), or [pyright](https://pydevtools.com/handbook/reference/pyright.md) as your checker, since the output is plain Python annotations.

Run it on a single directory at a time, review the diff, and commit before continuing. Then return to this guide to configure overrides and start on the modules that the codemod left untouched.

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

```toml
[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:

```toml
[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:

```bash
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](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) 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:

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

## Use Pyrefly's first-party adoption tools

[Pyrefly](https://pydevtools.com/handbook/reference/pyrefly.md) 1.0 ships two adoption helpers that match the strategy in this guide and remove the need to roll your own tooling.

`pyrefly coverage report` writes a JSON report of annotation completeness and type completeness per function, class, and module. Commit the report under `.pyrefly/` or send it to a dashboard to track how the typed surface grows over time. See the [coverage report docs](https://pyrefly.org/en/docs/report/).

Baseline files replace inline `# pyrefly: ignore` comments with a snapshot of the current error set:

```bash
uv run pyrefly check --baseline=.pyrefly/baseline.json --update-baseline
```

Subsequent `pyrefly check --baseline=.pyrefly/baseline.json` runs report only errors not present in the baseline, so new code is checked strictly while legacy errors stay silent in the file rather than scattered across source. The mode is experimental in 1.0; see [Pyrefly's baseline files docs](https://pyrefly.org/en/docs/error-suppressions/#baseline-files-experimental).

## Learn More

- [How do mypy, pyright, and ty compare?](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md)
- [How to try the ty type checker](https://pydevtools.com/handbook/how-to/how-to-try-the-ty-type-checker.md)
- [How to use ty in CI](https://pydevtools.com/handbook/how-to/how-to-use-ty-in-ci.md)
- [How to configure Claude Code with a Python type checker](https://pydevtools.com/handbook/how-to/how-to-configure-claude-code-with-a-python-type-checker.md)
- [How to migrate from mypy to ty](https://pydevtools.com/handbook/how-to/how-to-migrate-from-mypy-to-ty.md)
- [mypy reference](https://pydevtools.com/handbook/reference/mypy.md)
- [pyright reference](https://pydevtools.com/handbook/reference/pyright.md)
- [ty reference](https://pydevtools.com/handbook/reference/ty.md)
- [Pyrefly reference](https://pydevtools.com/handbook/reference/pyrefly.md)
- [Pyrefly coverage report docs](https://pyrefly.org/en/docs/report/)
- [Pyrefly baseline files docs](https://pyrefly.org/en/docs/error-suppressions/#baseline-files-experimental)
- [mypy existing code documentation](https://mypy.readthedocs.io/en/stable/existing_code.html)
