# How to migrate from mypy to Pyrefly


[Pyrefly](https://pydevtools.com/handbook/reference/pyrefly.md) 1.0 ships an auto-migration tool that reads an existing [mypy](https://pydevtools.com/handbook/reference/mypy.md) 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](https://github.com/pre-commit/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](https://pydevtools.com/handbook/reference/uv.md) project:

```bash
uv add --dev pyrefly
```

For a pip project:

```bash
pip install --upgrade pyrefly
```

## Run `pyrefly init`

From the project root:

```bash
uv run pyrefly init
```

The output names the source config:

```console
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:

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

[errors]
deprecated = "error"
```

The `legacy` preset disables three specific Pyrefly check kinds that mypy lacks: `bad-override-mutable-attribute`, `bad-override-param-name`, and `unbound-name`. Pyrefly's other rules (such as stricter Protocol conformance, which also checks parameter names) stay on, so the codebase will still see diagnostics mypy did not produce. 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:

```console
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:

```bash
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`:

```toml {filename="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:

```console
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/`:

```yaml {filename=".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](https://pyrefly.org/en/docs/error-suppressions/) 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](https://pydevtools.com/handbook/how-to/how-to-gradually-adopt-type-checking-in-an-existing-python-project.md) 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](https://pyrefly.org/en/docs/migrating-from-mypy/) 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:

```bash
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

- [Pyrefly reference](https://pydevtools.com/handbook/reference/pyrefly.md)
- [How do Python type checkers compare?](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md)
- [How to gradually adopt type checking in an existing Python project](https://pydevtools.com/handbook/how-to/how-to-gradually-adopt-type-checking-in-an-existing-python-project.md)
- [Pyrefly configuration docs](https://pyrefly.org/en/docs/configuration/)
- [Pyrefly error suppressions](https://pyrefly.org/en/docs/error-suppressions/)
- [Pyrefly baseline files (experimental)](https://pyrefly.org/en/docs/error-suppressions/#baseline-files-experimental)
