Skip to main content

Pants Integration

The Pants integration is provided by the community plugin pants-pyrefly (available on PyPI; check there for the latest version). Installing it makes Pyrefly a checker in the Pants check goal, so pants check :: type-checks your repository.

Pants downloads the official prebuilt Pyrefly binary (pinned by SHA256) and runs it hermetically in a sandbox. Instead of maintaining a pyrefly.toml search-path by hand, the plugin derives Pyrefly's first-party search paths from your Pants source roots and points Pyrefly at a venv built from each target's resolved third-party requirements, so imports resolve the same way they do at runtime.

When to use it

  • Your Python code is already built with Pants and you want pants check to type-check it with Pyrefly, using the same source roots, resolves, and interpreter constraints Pants already knows about.
  • You are moving a Pants repo off mypy and want the migration to happen through Pants — config conversion, incremental adoption, and CI gating — rather than as a separate out-of-band pyrefly invocation.

If you do not use Pants, use pyrefly check as before.

Unlike the Bazel integration, the Pants plugin does read pyrefly.toml (or pyproject.toml [tool.pyrefly]): strictness and per-directory policy stay in your Pyrefly config, while the plugin supplies the build-graph facts — search paths, interpreter, and the file list — plus a few reporting overrides (output_format, min_severity, only, baseline) that it passes on the command line, where they take precedence over the config file.

Setup

1. Install the plugin

Add the plugin and enable its backend in pants.toml. Keep your existing pants.backend.python entry; pants_pyrefly is added alongside it:

[GLOBAL]
plugins = ["pants-pyrefly==PANTS_PYREFLY_VERSION"]
backend_packages.add = [
"pants.backend.python",
"pants_pyrefly",
]

Replace PANTS_PYREFLY_VERSION with the latest release. The published wheel is pure Python and declares no pantsbuild.pants dependency, so one release installs into any supported Pants version; see the plugin's compatibility table for the supported Pants range and the Pyrefly version each plugin release pins.

To pin a different Pyrefly than the plugin's default, set [pyrefly].version (with matching [pyrefly].known_versions).

To vendor the plugin instead — for rapid iteration or to pin to an exact source state — copy pants-plugins/pants_pyrefly/ into your repo and load it the way in-repo plugins are normally loaded:

[GLOBAL]
pythonpath = ["%(buildroot)s/pants-plugins"]
backend_packages.add = ["pants.backend.python", "pants_pyrefly"]

If you keep plugin code in its own resolve, add it there and run pants generate-lockfiles.

2. Create a Pyrefly config

pants pyrefly-init wraps pyrefly init. Running it through Pants means the plugin supplies the Pyrefly binary, so you do not need Pyrefly installed separately:

pants pyrefly-init

If your build root has a pyproject.toml, this adds a [tool.pyrefly] table to it; otherwise it creates a pyrefly.toml. Either way it refuses to overwrite an existing Pyrefly config — remove that first to regenerate.

Migrating an existing checker? If the repo has a mypy configuration, init translates it instead of writing defaults; go to Bootstrapping from Mypy below and run that section's command instead of the bare pants pyrefly-init above, since init will not overwrite the config a plain run just wrote. Pyright configurations are also migrated, but only from [tool.pyright] in pyproject.toml: the plugin does not stage pyrightconfig.json into the sandbox, so a config in that file is invisible to pyrefly-init.

Checking policy — strictness, per-directory overrides, error suppression — is configured here as documented in Configuration, not on the plugin. The plugin discovers this file automatically; use [pyrefly].config to point at a non-standard path.

3. Run

# one subtree
pants check path/to/dir::

# whole repo
pants check ::

Diagnostics are printed in Pyrefly's usual format, and anything at or above the reporting threshold — error by default, or whatever [pyrefly].min_severity sets — fails the goal. Targets are partitioned by resolve and interpreter constraints, and each partition gets its own Pyrefly invocation against the interpreter and requirements for that partition. If Pyrefly itself fails (an exit code other than "clean" or "found errors"), the plugin reports that as a tool failure rather than as type errors.

If more than one checker is enabled (during a mypy transition, for example), scope the goal to one of them. --only belongs to check, so it goes after the goal name:

pants check --only=pyrefly ::

4. Configure your editor

Pyrefly ships an LSP server, but on its own it does not know your Pants source roots, so first-party imports will not resolve. Write them into your Pyrefly config as search-path, along with the python-version your interpreter constraints imply:

pants pyrefly-lsp-config

For third-party imports, point your editor's interpreter at an exported venv (pants export --resolve=python-default). If your Pyrefly config lives in pyproject.toml under [tool.pyrefly], the goal prints the keys to add rather than writing a pyrefly.toml that would shadow it.

note

Pyrefly adds a config file's search-path to whatever is passed on the command line rather than replacing it, so the roots written here are also in effect during pants check. That is usually harmless, but if one of them is the build root (.), it re-exposes every file under a second module name inside the sandbox — the exact problem the staging described in How it works exists to prevent. If check starts reporting duplicate-module errors right after you run this goal, that is the cause.

Configuration

Options live in the [pyrefly] subsystem in pants.toml, and each has a corresponding command-line flag:

OptionDescription
skipDon't run Pyrefly during check.
argsExtra arguments passed to Pyrefly, e.g. --pyrefly-args='--python-version 3.12'.
output_formatOverride Pyrefly's output format — any value --output-format accepts, e.g. json, github (GitHub Actions annotations), junit-xml, sarif.
min_severityOnly report errors at or above this severity: ignore, info, warn, error.
onlyOnly report these error kinds, e.g. bad-assignment. Useful for triaging one category at a time.
config / config_discoveryPoint at a Pyrefly config in a non-standard location, or turn off automatic discovery.
baselinePath to a Pyrefly baseline JSON; check then reports only errors new since the baseline.
extra_type_stubsStub-only packages to add to the type-check environment without making them runtime dependencies, e.g. types-requests. Resolved directly rather than from a lockfile, so pin versions.
version / known_versions / url_templatePin or override the downloaded Pyrefly binary.

Opt an individual target out of Pyrefly with the skip_pyrefly field:

python_sources(skip_pyrefly=True)

config, config_discovery, extra_type_stubs, and the binary-pinning options are advanced; pants help-advanced pyrefly lists them alongside [pyrefly].interpreter_constraints, the fallback used for targets that declare none of their own. See also the plugin's Configuration section.

Bootstrapping a configuration from Mypy

Pyrefly can read an existing mypy configuration and produce an equivalent pyrefly.toml, so you start from your repo's established strictness and per-module policy rather than from Pyrefly's defaults.

The migration reads mypy's own config files: mypy.ini, or [tool.mypy] in pyproject.toml. Two things it will not pick up:

  • setup.cfg. Pyrefly's migration does not read it; move a [mypy] section there into mypy.ini first.
  • Pants's mypy subsystem options. [mypy].args and friends in pants.toml are Pants configuration rather than mypy configuration. If that is where all your mypy settings live, there is nothing on disk to migrate and you should configure Pyrefly from scratch instead.

1. Install alongside Mypy

Follow Install the plugin above, but keep the mypy backend enabled during the transition so you can run both checkers and compare their output:

[GLOBAL]
backend_packages.add = [
"pants.backend.python",
"pants.backend.python.typecheck.mypy", # keep during the transition
"pants_pyrefly",
]

pants check :: now runs both. Scope to one with check's --only:

pants check --only=pyrefly ::
pants check --only=mypy ::

2. Convert the config

pants pyrefly-init --pyrefly-init-migrate-from=mypy

This runs pyrefly init --migrate-from mypy and writes the translated settings to pyrefly.toml. Passing mypy explicitly forces mypy as the source; with no flag, init auto-detects (mypy, then Pyright).

When init detects a mypy config it selects the legacy preset, which turns off a few checks mypy does not have so the first Pyrefly run does not bury you in error classes mypy never flagged. Behavior still differs between the two checkers; the preset narrows the gap rather than closing it.

Review the generated config — the mapping is close but not one-to-one. Broad settings like ignore_missing_imports, per-module overrides, and strictness flags carry over. mypy plugins do not: Pyrefly has no plugin system, so a plugins = entry is dropped.

For most of the widely used mypy plugins this does not matter, because Pyrefly supports the same libraries natively and automatically — Pydantic, Django's ORM, attrs, and DataFrames. Where a plugin has no native counterpart — SQLAlchemy's declarative-ORM plugin is the common case, and Pydantic v1, which Pyrefly deliberately does not support — the dynamic attributes it synthesized will surface as errors, and stub packages such as sqlalchemy2-stubs only partly bridge the gap. For those packages, either suppress the errors, fold them into a baseline, or keep mypy enabled for just those packages during the transition.

Migrating from Mypy covers the checker-level differences — command-line equivalents, suppression syntax, and behavioral divergences — independently of Pants.

3. Adopt incrementally with a baseline

A large codebase will have pre-existing errors under a stricter configuration. Record them in a baseline so check only fails on new ones. Set the path in pants.toml — the plugin materializes the baseline into its sandbox only when [pyrefly].baseline names it, so a baseline key in pyrefly.toml has no effect here — and commit the file:

[pyrefly]
baseline = "build-support/pyrefly-baseline.json"
pants pyrefly-update-baseline ::   # record current errors
pants check :: # now reports only errors introduced since

Re-run pants pyrefly-update-baseline as you fix errors to ratchet the baseline down.

Prefer inline suppressions? pants pyrefly-suppress :: rewrites the targeted files in place, adding a # pyrefly: ignore comment on the line above each error (see Error Suppressions); delete them as you fix, or run pants pyrefly-suppress --pyrefly-suppress-remove-unused :: to strip stale ones. It edits your working tree, so run it on a clean checkout. A baseline file and inline suppressions solve the same problem — pick one.

4. Track progress

pants pyrefly-coverage reports the share of typable symbols that have a non-Any type — Pyrefly's strict coverage, not the looser metric that counts Any as covered (see Measuring Type Coverage for both). It works well as a migration ratchet in CI, though its threshold is therefore not comparable to a pyrefly coverage check --fail-under number:

pants pyrefly-coverage ::
pants pyrefly-coverage --pyrefly-coverage-fail-under=80 ::

5. Remove Mypy

Once Pyrefly is green (or a baseline established) and CI gates on it, drop pants.backend.python.typecheck.mypy from backend_packages and delete the mypy configuration.

Goals

Besides participating in check, the plugin adds these goals:

GoalPurpose
pyrefly-initBootstrap pyrefly.toml, migrating a mypy or Pyright config when present.
pyrefly-lsp-configWrite Pants's source roots into pyrefly.toml as search-path for the IDE/LSP.
pyrefly-update-baselineRecord current errors in the baseline named by [pyrefly].baseline.
pyrefly-suppressAdd (or, with --remove-unused, strip) inline # pyrefly: ignore comments.
pyrefly-coverageReport type coverage, optionally failing under a threshold.
pyrefly-dump-configPrint the effective Pyrefly configuration Pants assembles. Diagnostic only.

How it works

Each partition of targets — grouped by resolve and interpreter constraints — gets one Pyrefly invocation over that partition's sources.

  • First-party code resolves through --search-path, the analogue of MYPYPATH, derived from your Pants source roots. Because Pants gives each file exactly one source root while Pyrefly makes a file importable under every search path containing it, nested source roots would otherwise give a module two identities. The plugin removes the nesting structurally: it re-stages each source root's files into its own non-nesting sibling directory in the sandbox and passes --disable-search-path-heuristics, then maps the synthetic paths back to real repo paths in diagnostics, baseline files, and suppress edits — so it is invisible in the output. The diagnostic goals (pyrefly-coverage, pyrefly-dump-config, pyrefly-lsp-config) skip the staging and pass your real roots, deduplicated to each file's nearest root; [pyrefly].exclude_source_roots force-drops a root on that path only, and has no effect on check.
  • Third-party dependencies are materialized into a venv, and Pyrefly's --python-interpreter-path points at it, so Pyrefly discovers site-packages and the target Python version exactly as import would at runtime.

When imports or the interpreter resolve differently than you expect, dump the configuration Pants actually assembles:

pants pyrefly-dump-config ::               # whole repo
pants pyrefly-dump-config src/project:: # a subtree

This runs Pyrefly's dump-config over the same partitions and file list as check, so it is the right tool for confirming which config file, interpreter, and source roots a given target resolves against. Two differences from check worth knowing when you read the output: dump-config does not re-stage sources, so the search-path it prints is your real source roots (nearest-root deduped) rather than the synthetic sandbox roots check passes; and the reporting options — output_format, min_severity, only, baseline, and [pyrefly].args — apply to check only, so they do not appear here.

Common causes of resolution problems:

  • A first-party module reports a missing import — it is not reachable from a source root. pants roots lists them.
  • A third-party package reports a missing import — the requirement is not in that target's resolve, or it ships no types. Add it as a dependency; add stub-only packages through [pyrefly].extra_type_stubs.
  • A whole partition fails to find an interpreter — no installed interpreter matches that partition's interpreter_constraints.

For implementation details and the plugin's compatibility promise, see the pants-pyrefly README.