A Pants plugin that runs Pyrefly —
Meta's fast, Rust-based Python type checker — as part of the Pants check goal.
Pants downloads the official prebuilt Pyrefly binary (pinned by SHA256) and runs it hermetically in a sandbox, wiring up your first-party source roots and the resolved third-party dependencies so that imports resolve correctly.
- Pants 2.27–2.32. A single codebase supports both the legacy (
Get/MultiGet-era) and modern (call-by-name) rules APIs via a small version-conditional import; verified on 2.27 and 2.32. - The published wheel is pure-Python —
Requires-Python: >=3.11, with nopantsbuild.pantsdependency (Pants provides itself at runtime) — so a single release installs into any supported Pants, from 2.27 (CPython 3.11) through 2.32 (CPython 3.14).
Add the plugin and enable its backend in pants.toml:
[GLOBAL]
plugins = ["pants-pyrefly==0.3.0"]
backend_packages.add = [
"pants.backend.python",
"pants_pyrefly",
]Prefer to vendor the plugin — for rapid iteration, or to pin to an exact source state? Consume it
the way in-repo plugins are normally loaded: copy pants-plugins/pants_pyrefly/ into your repo and:
[GLOBAL]
pythonpath = ["%(buildroot)s/pants-plugins"]
backend_packages.add = ["pants.backend.python", "pants_pyrefly"]If you keep plugin code in a dedicated pants-plugins resolve, add it there and run
pants generate-lockfiles.
Bootstrap a Pyrefly config for the repo (wraps pyrefly init). If you already have a MyPy or
Pyright configuration, it is migrated into the new pyrefly.toml:
pants pyrefly-init # create pyrefly.toml (auto-migrates mypy/pyright)
pants pyrefly-init --pyrefly-init-migrate-from=mypy # force migrating from a MyPy configIt refuses to overwrite an existing pyrefly.toml (or a [tool.pyrefly] table in
pyproject.toml) — remove it first to regenerate. Then run pants pyrefly-lsp-config (see
Editor / IDE) so your editor resolves first-party imports the way Pants does.
pants check :: # type-check everything
pants check path/to/dir:: # type-check a subtree[pyrefly] subsystem options:
| Option | Env / flag | Description |
|---|---|---|
skip |
--pyrefly-skip / PANTS_PYREFLY_SKIP |
Don't run Pyrefly during check. |
args |
--pyrefly-args |
Extra args passed to Pyrefly, e.g. --pyrefly-args='--python-version 3.12'. |
extra_type_stubs |
--pyrefly-extra-type-stubs |
Stub-only packages to add to the type-check environment without making them runtime deps, e.g. types-requests, sqlalchemy2-stubs==0.0.2a38. Resolved directly, so pin versions for reproducibility. |
output_format |
--pyrefly-output-format |
Override Pyrefly's output format: min-text, full-text, json, github, junit-xml, omit-errors. |
min_severity |
--pyrefly-min-severity |
Only show errors at/above this severity (ignore/info/warn/error). |
only |
--pyrefly-only |
Only report these error kinds (e.g. bad-assignment); handy for triage. |
config |
--pyrefly-config |
Path to a pyrefly.toml / pyproject.toml (disables discovery). |
config_discovery |
--[no-]pyrefly-config-discovery |
Auto-discover pyrefly.toml / [tool.pyrefly]. |
baseline |
--pyrefly-baseline |
Path to a Pyrefly baseline JSON; check then reports only errors new since the baseline. Generate it with pants pyrefly-update-baseline. |
version / known_versions / url_template |
(advanced) | Pin or override the downloaded Pyrefly binary. |
Opt a target out of Pyrefly:
python_sources(skip_pyrefly=True)Adopting Pyrefly on a codebase that already has type errors? Record them in a baseline so check
only fails on new errors:
pants pyrefly-update-baseline :: # writes the file named by [pyrefly].baseline
pants check :: # now reports only errors introduced since the baselineConfigure the path (and commit the baseline file):
[pyrefly]
baseline = "build-support/pyrefly-baseline.json"Re-run pants pyrefly-update-baseline after fixing errors, or to refresh it. Baseline matching is
Pyrefly's own (lenient by design, so it survives code churn).
Prefer inline suppressions? pants pyrefly-suppress :: instead rewrites the targeted files in
place, adding # pyrefly: ignore on each current error (Pyrefly's suppress); delete them as you
fix, or run pants pyrefly-suppress --pyrefly-suppress-remove-unused :: to strip stale ones. An
external baseline (JSON) and inline suppressions are two strategies for the same goal — pick one.
Moving a Pants repo off MyPy? See docs/migrating-from-mypy.md —
config conversion (pyrefly init --migrate-from mypy), running both checkers during the transition,
baseline-based incremental adoption, and the MyPy-plugin gap (SQLAlchemy et al.).
Pyrefly ships an LSP server, but in a Pants repo your editor doesn't know the source roots. Generate
a pyrefly.toml with them:
pants pyrefly-lsp-config # writes search-path (= your source roots) + python-versionFor third-party imports, point your editor's interpreter at a venv (e.g.
pants export --resolve=python-default). If your Pyrefly config lives in pyproject.toml
[tool.pyrefly], the goal prints the keys to add instead of writing a shadowing pyrefly.toml.
Track typing progress — useful as a migration ratchet:
pants pyrefly-coverage :: # prints overall % typed
pants pyrefly-coverage --pyrefly-coverage-fail-under=80 :: # also fails if below 80%- First-party code: every source root is passed to Pyrefly via
--search-path(the analogue ofMYPYPATH/sys.path). - Third-party deps: Pants materializes the target's resolved requirements into a venv and points
Pyrefly's
--python-interpreter-pathat it, so Pyrefly discoverssite-packagesand the target Python version exactly asimportwould at runtime.
When Pyrefly resolves imports or the interpreter differently than you expect, dump the effective
configuration Pants assembles — the first-party search-paths, the interpreter used for third-party
resolution, and the config file in effect:
pants pyrefly-dump-config :: # whole repo
pants pyrefly-dump-config src/project:: # a subtreeThis runs Pyrefly's dump-config subcommand with exactly the arguments Pants passes to check, so
what you see is what pants check sees. It does not type-check. When targets span multiple resolves
or interpreter constraints, each partition's config is printed under its own heading.
| Plugin version | Pants | Pyrefly (default) |
|---|---|---|
0.3.0 |
2.27–2.32 |
1.1.1 |
0.2.0 |
2.27–2.32 |
1.1.1 |
0.1.0 |
2.27–2.32 |
1.1.1 |
The plugin supports both the legacy (Get/MultiGet) and modern (call-by-name) rules APIs through
a small version-conditional import (the rules API changed at Pants 2.30, and again removed Get
by 2.32). Verified on 2.27 and 2.32; in-between versions use the same modern API.
This repo dogfoods its own tooling: ruff (lint + format) and Pyrefly itself (check) run on the
plugin's sources.
pants generate-lockfiles # pants-plugins + python-default resolves
pants fmt lint :: # ruff format + check
pants check :: # Pyrefly type-checks the plugin (dogfood) + testprojects/
pants test :: # run the integration tests
pants package pants-plugins/pants_pyrefly:dist # build the wheel + sdist into dist/The four default_known_versions pins in subsystems.py (<version>|<platform>|<sha256>|<size>)
are generated, not hand-edited. To move to a new Pyrefly release:
python3 build-support/bin/generate_known_versions.py --version <new> --writeIt reads the URL template and platform mapping straight from subsystems.py, fetches each asset's
published .sha256 sidecar and size from the GitHub release, and rewrites default_version + the
pins. CI runs the same script with --check and fails if the committed pins drift from what the
release actually publishes. (Set GITHUB_TOKEN to avoid GitHub API rate limits.)
Push a vX.Y.Z tag. The release workflow builds the wheel and
publishes it to PyPI using Trusted Publishing (OIDC,
no API tokens). Configure a PyPI trusted publisher for this repo + the release.yml workflow first.