Skip to content

How to migrate from mypy to Pyrefly

Pyrefly 1.0 ships an auto-migration tool that reads an existing mypy config and writes the equivalent Pyrefly configuration. The mechanical translation is one command. The diagnostic triage that follows is the real work, and Pyrefly’s incremental-adoption tooling makes it possible to do that work without blocking CI.

This guide walks through both halves using pre-commit as a real example.

Confirm your mypy config location

pyrefly init reads two file formats:

  • mypy.ini or .mypy.ini at the project root
  • [tool.mypy] in pyproject.toml

If the project’s mypy config lives in setup.cfg (under a [mypy] section), pyrefly init will not detect it. Copy the [mypy] section out to a new mypy.ini first, or move it to [tool.mypy] in pyproject.toml. The setup.cfg pattern is common in older projects; pre-commit itself keeps its mypy config there as of May 2026.

Install Pyrefly

Add Pyrefly as a development dependency in a uv project:

uv add --dev pyrefly

For a pip project:

pip install --upgrade pyrefly

Run pyrefly init

From the project root:

uv run pyrefly init

The output names the source config:

INFO Found an existing type checking configuration - setting up pyrefly ...
INFO Migrating mypy config file from: `mypy.ini`
INFO New config written to `pyrefly.toml`

For pre-commit’s [mypy] block (check_untyped_defs, disallow_any_generics, disallow_untyped_defs, enable_error_code = deprecated, plus per-module overrides for tests and testing), the generated pyrefly.toml looks like:

pyrefly.toml
preset = "legacy"
check-unannotated-defs = true
infer-return-types = "never"

[errors]
deprecated = "error"

The legacy preset disables checks mypy does not have at all. The remaining settings map mypy’s behavior onto Pyrefly’s check kinds. pyrefly init writes infer-return-types = "never" here because mypy does not aggressively infer return types, and the migration tool preserves that behavior.

Snapshot the diagnostics before running CI

pyrefly init invokes a check at the end. On a real codebase, this is where the friction shows up. Running against pre-commit’s main branch as of May 12, 2026 (six commits past tag v4.6.0) produces 116 diagnostics on the first pass:

INFO Checking project configured at `pyrefly.toml`
INFO 116 errors (1 suppressed)

About 47 of those are missing-import errors for un-stubbed third-party packages (cfgv, setuptools) that mypy also flags. The remaining ~69 are new diagnostics from Pyrefly’s inference: argument-type mismatches and missing-attribute cases where super().method() calls reference methods the parent class does not actually define. Most look like legitimate type bugs mypy missed rather than spurious noise.

Triaging 69 new diagnostics in a single PR is the same problem the legacy preset was designed to avoid, just shifted to a different layer. Baseline files are the answer:

uv run pyrefly check --baseline=pyrefly-baseline.json --update-baseline

That command writes the current error set into pyrefly-baseline.json. Subsequent runs pick it up automatically through the baseline = "pyrefly-baseline.json" setting that should be added to pyrefly.toml:

pyrefly.toml
preset = "legacy"
check-unannotated-defs = true
infer-return-types = "never"
baseline = "pyrefly-baseline.json"

[errors]
deprecated = "error"

After that, pyrefly check returns clean and only reports new diagnostics introduced by future changes:

INFO 0 errors (1 suppressed)

Commit both pyrefly.toml and pyrefly-baseline.json. The baseline files feature is flagged experimental upstream; the format may shift between Pyrefly releases, so pin a specific Pyrefly version in pyproject.toml until the format stabilizes.

Replace mypy in CI

Once pyrefly check returns clean against the baseline, swap the type-check command in CI. For a GitHub Actions step that previously ran mypy pre_commit/:

.github/workflows/test.yml
- name: Type check
  run: uv run pyrefly check

Run the workflow on a branch and confirm it reports zero errors before merging. The baseline file moves with the repo, so any developer running uv run pyrefly check locally sees the same numbers as CI.

Triage the baselined diagnostics over time

Each diagnostic in the baseline is either a missing-stub error mypy also reports or a place Pyrefly’s inference surfaced a finding mypy missed. Triage them in batches:

  • Update annotations where Pyrefly’s inference is correct and mypy was permissive.
  • Add # pyrefly: ignore[error-code] suppressions where the diagnostic is wrong and Pyrefly’s own error-suppressions docs describe the syntax. mypy’s # type: ignore comments do not transfer.
  • Re-snapshot the baseline whenever a batch is resolved: uv run pyrefly check --baseline=pyrefly-baseline.json --update-baseline.

The pyrefly report command emits a JSON summary of annotation completeness per function and module, which is useful for tracking progress in CI without parsing diagnostic counts. See how to gradually adopt type checking in an existing Python project for the broader pattern.

Inline ignores as an alternative

pyrefly suppress is the alternative to a baseline file. It walks the codebase and adds inline # pyrefly: ignore[error-code] comments at each existing error site. Pyrefly’s own migration docs recommend this pattern. Baseline files keep the source tree clean and the suppression off-source; inline ignores live at the call site, which makes per-error context discoverable but pollutes diffs across the codebase. The baseline file format is also still flagged experimental upstream. Pick whichever matches the team’s preference.

Remove mypy

When the baseline is empty (or shrunk to a residue the team is comfortable suppressing inline), drop mypy from the project’s dev dependencies and delete the source config:

uv remove --dev mypy types-PyYAML types-cachetools
rm mypy.ini  # or delete [tool.mypy] from pyproject.toml

Keep any # type: ignore comments in place for now. They are silent to Pyrefly and harmless. A follow-up sweep can convert them to # pyrefly: ignore[error-code] form, but it is not blocking.

Learn More

Last updated on