pants-pyrefly
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.
Requirements
- Pants 2.27–2.33. 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.33. - 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.33 (CPython 3.14).
Installation
Add the plugin and enable its backend in pants.toml:
[GLOBAL]
plugins = ["pants-pyrefly==1.0.0"]
backend_packages.add = [
"pants.backend.python",
"pants_pyrefly",
]
From source (in-repo)
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.
Getting started
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 config
It 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.
Usage
pants check :: # type-check everything
pants check path/to/dir:: # type-check a subtree
Configuration
[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. |
exclude_source_roots |
--pyrefly-exclude-source-roots (advanced) |
Source roots to omit from --search-path. Rarely needed — nested roots are deduped automatically (see below); use this only to force-drop a root the automatic logic keeps. |
version / known_versions / url_template |
(advanced) | Pin or override the downloaded Pyrefly binary. |
Opt a target out of Pyrefly:
python_sources(skip_pyrefly=True)
Incremental adoption (baseline)
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 baseline
Configure 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.
Migrating from MyPy
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.).
Editor / IDE (LSP)
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-version
For 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.
Type coverage
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%
How import resolution works
-
First-party code: your source roots are passed to Pyrefly via
--search-path(the analogue ofMYPYPATH/sys.path). Pants gives every file exactly one source root, but Pyrefly makes a file importable under every search path that physically contains it — so when source roots nest (the common case: the build root.abovesrc/python), a file gets two module identities (pkg.modandsrc.python.pkg.mod) and Pyrefly reports spurious errors where one flows into the other.For
checkandpyrefly-suppress, the plugin removes the nesting structurally: each source root's files are re-staged in the sandbox under its own sibling directory (__pyrefly_root_<n>) with the root prefix stripped, and each of those is passed as a single--search-pathalongside--disable-search-path-heuristics. Sibling directories can't nest, so every file is reachable under exactly one module identity no matter howroot_patternsoverlap. Pyrefly's synthetic paths are mapped back to real repo paths in diagnostics, baseline files, andsuppressedits, so this is invisible in output.The diagnostic goals (
pyrefly-coverage,pyrefly-dump-config,pyrefly-lsp-config) don't re-stage — they pass your real source roots, deduplicated to each file's nearest root. If first-party code genuinely roots at both an ancestor and a nested root, both are kept and the plugin warns;[pyrefly].exclude_source_rootsforce-drops one. -
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.
Diagnostics
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 subtree
This 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.
Pants compatibility
| Plugin version | Pants | Pyrefly (default) |
|---|---|---|
1.0.0 |
2.27–2.33 |
1.2.0 |
0.5.0 |
2.27–2.32 |
1.1.1 |
0.4.0 |
2.27–2.32 |
1.1.1 |
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). CI smoke-tests consumption on 2.27, 2.31, 2.32, and 2.33; in-between versions use the
same modern API.
Stability
From 1.0.0 on, this project follows Semantic Versioning. Covered by the compatibility promise — a breaking change to any of these requires a major bump:
- The goal names (
pyrefly-init,pyrefly-lsp-config,pyrefly-coverage,pyrefly-suppress,pyrefly-update-baseline,pyrefly-dump-config) and Pyrefly's participation incheck. - The
[pyrefly]option names documented under Configuration, and theskip_pyreflyfield. - The backend name
pants_pyrefly, and the published wheel carrying nopantsbuild.pantsdependency.
Not covered: the plugin's Python API (every module is an implementation detail — import nothing
from pants_pyrefly directly), the default pinned Pyrefly version, the exact wording and layout of
Pyrefly's own diagnostic output, and the sandbox staging mechanics described under
How import resolution works. Dropping a Pants version that has
reached end of life is a minor bump, not a major one.
Development
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/
Bumping the pinned Pyrefly version
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> --write
It 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.)
Releasing
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.
License
Metadata
Release files for pants-pyrefly 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pants_pyrefly-1.0.0.tar.gz | 27.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pants_pyrefly-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 52.7 kB
Release files / pants_pyrefly-1.0.0.tar.gz
| Download URL | pants_pyrefly-1.0.0.tar.gz |
|---|---|
| Size | 27.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c74251d9041ea2ed7d5bc5c1ece53b90eea8b70cd761dbcef575afbd6de3948c
|
|
BLAKE2b-256 checksum How to use checksums |
ded1177e91ff739e7f178928c657b9674afea8be3b22fda4e71ea34eab9a1bff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency logRelease files / pants_pyrefly-1.0.0-py3-none-any.whl
| Download URL | pants_pyrefly-1.0.0-py3-none-any.whl |
|---|---|
| Size | 25.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3340478da198419dbcb7d9295170a21f40565afa7b13f6a05dd7b7d988e1f6c5
|
|
BLAKE2b-256 checksum How to use checksums |
fa75af51a85a2e72a350420d606aa32a7718fe08351f358eeb4a792ff153afff
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency log