How to migrate from mypy to Pyrefly
Pyrefly converts supported mypy configuration, but its diagnostics and inference differ. Keep mypy in CI until the converted settings and a representative set of framework calls have been checked.
Install Pyrefly and preview the conversion
From the root of a project managed by uv:
uv add --dev pyrefly
uv run pyrefly init --dry-run --print-configThe --dev flag marks Pyrefly 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.
The preview prints the proposed TOML without changing configuration. Pyrefly migrates mypy.ini or [tool.mypy] in pyproject.toml. It does not discover .mypy.ini or setup.cfg for conversion. Copy their [mypy] settings into mypy.ini first.
Write and review the configuration
uv run pyrefly init --non-interactiveThe command writes pyrefly.toml, or adds [tool.pyrefly] when migrating pyproject.toml, then runs a check. It leaves mypy’s configuration in place. It may exit successfully even when that initial check reports errors, so run pyrefly check separately before accepting the migration.
Review every setting against Pyrefly’s mypy configuration mapping. Unsupported settings may be dropped without a warning. Do not assume that mypy plugins or every per-module strictness option survive conversion.
The converter uses preset = "legacy" and normally disables return inference to preserve mypy’s behavior. A project with check_untyped_defs = true should have:
preset = "legacy"
check-unannotated-defs = true
infer-return-types = "never"This is only the core behavior mapping, not a replacement for the full generated file. Preserve converted import paths and strictness rules the project needs. Without check_untyped_defs, the converter can set check-unannotated-defs = false instead.
Compare diagnostics before recording a baseline
uv run mypy src/
uv run pyrefly checkThe first command should retain the project’s existing mypy result. Review Pyrefly’s additional diagnostics as possible bugs, inference differences, missing dependency information, or unsupported framework behavior. A higher error count alone does not establish better coverage.
Before collecting the experimental baseline, add this top-level setting to pyrefly.toml (or its [tool.pyrefly] table):
baseline-matching-mode = "column-ordered"The default matching mode allows one baseline entry to suppress several diagnostics sharing its file, error kind, and column. column-ordered limits suppression to the recorded sequence, so additional occurrences are reported. Then collect the reviewed backlog:
uv run pyrefly check --baseline=pyrefly-baseline.json --update-baseline
uv run pyrefly check --baseline=pyrefly-baseline.jsonThe update command writes the baseline and can still exit 1 while printing existing errors. The second command should report zero errors and exit 0 when every diagnostic matches the baseline. Add this setting to the same configuration and commit it with the baseline:
baseline = "pyrefly-baseline.json"Pin Pyrefly through the committed lockfile. Re-generating a baseline accepts all current diagnostics, including newly introduced ones, so review the diff each time.
Replace the CI check after a parallel trial
After the workflow installs the project’s locked dependencies, use:
- name: Type check
run: uv run --locked pyrefly checkWith the committed configuration and baseline, this fails on errors outside the baseline. Keep the existing mypy step until framework and configuration differences have been reviewed.
Shrink the migration backlog
Fix diagnostics in small batches. To remove resolved baseline entries without accepting new errors:
uv run pyrefly check --baseline=pyrefly-baseline.json --prune-baselineThe baseline loses stale entries; unmatched diagnostics still appear and fail the check. Alternatively, uv run pyrefly suppress inserts inline comments at existing error locations. Review that source diff before committing it.
Use # pyrefly: ignore[bad-assignment] for a targeted assignment exception. A bare # type: ignore suppresses Pyrefly diagnostics on that line; review inherited mypy ignores before assuming they are still necessary.
Remove mypy while retaining needed stubs
Once Pyrefly alone provides the intended CI and editor coverage:
uv remove --dev mypyuv updates the project and lockfile. Remove mypy’s configuration and CI step, but keep any stub packages Pyrefly still uses. Track the remaining baseline as migration debt rather than evidence that the code has no type errors.