aspect_rules_py provides its own Python interpreter provisioning, backed by
python-build-standalone
(PBS). This replaces the need to use rules_python's python.toolchain() for
interpreter management.
No version manifests. Unlike rules_python, there is no checked-in table of
SHA256 hashes or version metadata to maintain. Interpreter versions and checksums
are discovered automatically from PBS release artifacts and cached in your
MODULE.bazel.lock.
Easy updates. To get new interpreter versions, add a PBS release date. That's it — no repinning, no manifest regeneration.
No editorial decisions. We don't decide which Python versions you can use. Any version published in a PBS release is available. Need Python 3.8? Add an older release date that includes it.
Windows and cross-platform support. 9 platforms are registered out of the box, including Windows (x86_64, aarch64, i686), Linux (glibc and musl), and macOS.
Native extension toolchains. Each PBS interpreter repository defines a
rules_python C toolchain over that archive's headers and required Windows
import libraries. Regular runtimes also expose stable-ABI headers.
Free-threaded runtimes expose only the full ABI; the abi3t ABI introduced
by PEP 803 is not yet modeled.
# MODULE.bazel
bazel_dep(name = "aspect_rules_py", version = "1.6.7") # Or later
interpreters = use_extension("@aspect_rules_py//py:extensions.bzl", "python_interpreters")
interpreters.toolchain(
python_version = "3.12",
)
interpreters.toolchain(python_version = "3.11")
use_repo(interpreters, "python_interpreters")
register_toolchains("@python_interpreters//:all")Select the build-wide Python version in .bazelrc:
common --@aspect_rules_py//py:python_version=3.12
That's all you need. The extension uses a set of default PBS release dates that cover Python 3.8 through 3.15. The newest available build for each requested version is selected automatically.
By default, the extension ships with a small set of release dates covering the
full range of available Python versions. Use configure() to pin to specific
releases or to include versions that have been dropped from newer releases:
interpreters = use_extension("@aspect_rules_py//py:extensions.bzl", "python_interpreters")
interpreters.configure(
releases = ["20260303", "20241002"],
)
interpreters.toolchain(python_version = "3.12")
interpreters.toolchain(python_version = "3.8") # Resolved from 20241002
use_repo(interpreters, "python_interpreters")
register_toolchains("@python_interpreters//:all")When multiple releases contain the same Python minor version, the newest release
is preferred. Only one configure() tag is allowed per module graph, and only
the root module's tag is honored — dependency modules may include configure()
without error, but it will be silently ignored.
For development workflows where you always want the newest PBS release:
interpreters.configure(releases = ["latest"])This resolves to the newest release via the GitHub releases API. Because the result depends on when it runs, this marks the extension as non-reproducible — Bazel will re-evaluate it on each invocation rather than caching it.
For CI and production builds, prefer explicit release dates.
If you host PBS releases on a mirror or maintain your own fork:
interpreters.configure(
releases = ["20260303"],
base_url = "https://my-mirror.example.com/pbs/releases/download",
)The base_url must point to a directory structure matching PBS releases, where
{base_url}/{date}/SHA256SUMS and {base_url}/{date}/{asset} are valid paths.
The configure() tag and the pre_release flag on toolchain() are only
honored from the root module. This gives the root module full control over the
build environment while allowing dependency modules to declare which Python
versions they need.
| Setting | Root module | Non-root module |
|---|---|---|
configure() |
Sets release search space and mirror | Silently ignored |
toolchain(python_version = ...) |
Adds to global set | Adds to global set |
toolchain(pre_release = True) |
Honored | Silently ignored |
If a dependency module requests a Python version that isn't available in any release configured by the root module, the build will fail with a clear error message identifying which module requested it.
There are two layers of version control, from broadest to most specific.
Set --@aspect_rules_py//py:python_version in .bazelrc to lock the
entire build to a specific version and keep the selection explicit in version
control:
# .bazelrc
common --@aspect_rules_py//py:python_version=3.12
This flag can also be overridden on the command line for one-off testing against a different version:
# Quick smoke-test on 3.11 without editing any files
bazel test //... --@aspect_rules_py//py:python_version=3.11Individual py_binary, py_venv_binary, py_test, and py_venv_test targets
can pin to a specific version using the python_version attribute. This takes
precedence over the flag:
load("@aspect_rules_py//py:defs.bzl", "py_binary")
py_binary(
name = "app",
srcs = ["app.py"],
python_version = "3.12",
)Use this when a target genuinely requires a specific version — for example a library that only supports 3.12+, or a test matrix that exercises multiple versions:
[py_venv_test(
name = "test_py%s" % v.replace(".", ""),
srcs = ["test.py"],
main = "test.py",
python_version = v,
) for v in ["3.11", "3.12", "3.13"]]| Mechanism | Scope | Set by |
|---|---|---|
--@aspect_rules_py//py:python_version |
Whole build | .bazelrc or command line |
python_version attribute |
Single target | BUILD.bazel |
The target attribute takes precedence over the build-wide flag.
The extension registers one normal PBS archive for each available Python version
and platform (the archive is chosen by build_config, see below). It also
registers an optimized free-threaded archive where PBS publishes one, currently
for Python 3.13 and newer. Normal mode is selected by default. Select
free-threaded mode with the build setting:
bazel build \
--@aspect_rules_py//py/private/interpreter:freethreaded=true \
//...interpreters.toolchain() chooses Python versions; the build setting above
selects runtime mode.
PBS publishes each interpreter in several build configurations. configure()
selects which one backs the normal (non-free-threaded) runtime, hub-wide:
interpreters.configure(
releases = ["20260303"],
build_config = "install_only_stripped",
)Supported values:
build_config |
Description |
|---|---|
install_only |
Optimized, minimal redistributable (default). |
install_only_stripped |
Same as install_only with debug symbols removed (smallest download). |
<opt>[+<opt>...]-full |
A full archive; each <opt> is one of debug, noopt, pgo, lto, in that order (e.g. pgo+lto-full, debug-full). |
Only install_only and install_only_stripped are published for every platform
listed below, so they are the only configurations usable as a hub-wide default.
Each platform family publishes different -full flavors — glibc Linux and macOS
get debug-full and pgo+lto-full, musl Linux gets debug-full, lto-full,
and noopt-full, and Windows gets only pgo-full — and platforms without the
selected archive get no normal-mode toolchain (their interpreter repos still
exist and fetch cleanly, so use_repo() imports, bazel fetch --all, and
bazel vendor keep working, but referencing anything in them fails with an
explanation). A build_config that matches no platform at all for a requested
version fails extension evaluation eagerly. debug-full comes closest to
hub-wide, covering every platform except Windows. Statically linked (+static)
configurations are rejected because extension modules resolve Python symbols from
the loading interpreter. Free-threading is a separate axis selected at build time
(see Runtime modes), so build_config must not name a
freethreaded suffix.
debug-full produces a Py_DEBUG interpreter (ABI flag d); PyPI ships no
matching wheels, so C-extension dependencies must be built from source. The
debug property does not carry across the free-threading axis: with a
debug-full hub, selecting free-threaded mode still yields the optimized
free-threaded archive (ABI flag t, not td).
The following platforms are registered by default:
| Platform | OS | Architecture |
|---|---|---|
aarch64-apple-darwin |
macOS | ARM64 |
x86_64-apple-darwin |
macOS | x86_64 |
aarch64-unknown-linux-gnu |
Linux (glibc) | ARM64 |
x86_64-unknown-linux-gnu |
Linux (glibc) | x86_64 |
aarch64-unknown-linux-musl |
Linux (musl) | ARM64 |
x86_64-unknown-linux-musl |
Linux (musl) | x86_64 |
x86_64-pc-windows-msvc |
Windows | x86_64 |
aarch64-pc-windows-msvc |
Windows | ARM64 |
i686-pc-windows-msvc |
Windows | x86 (32-bit) |
Not all Python versions are available on all platforms. Unavailable combinations are silently skipped during toolchain resolution.
All listed PBS platforms emit Python runtime registrations. PBS exec-tools registrations are emitted only for supported execution platforms: macOS, Windows, and GNU Linux. PBS-backed Linux exec registrations support glibc; musl interpreters remain available as Linux target runtimes.
This interpreter provisioning is designed to coexist with rules_python:
- The standard
@bazel_tools//tools/python:toolchain_typeis used for toolchain registration, so these interpreters work with all existing Python rules. - The
@rules_python//python/config_settings:python_versionflag is kept in sync with our own version flag via build transitions. - File-based runtimes registered with
rules_python'spy_runtime/py_runtime_pairremain usable by rules_py rules, which read the runtime fields structurally. System interpreters (apy_runtimewithinterpreter_pathrather than an in-buildinterpreterfile) are not supported; rules_py requires a registered, in-build interpreter. - Build actions that run an interpreter (wheel installation, site-packages
merging) resolve
@aspect_rules_py//py/private/toolchain:exec_tools_toolchain_type, registered byinterpreters.toolchain(). The exec interpreter follows the Python version flags when the hub provisions that version; otherwise it falls back to the hub's highest provisioned version — including the hub rules_py itself registers, so this resolves even in modules that provision interpreters only throughrules_python'spython.toolchain(). rules_py registers nothing underrules_python's exec-tools type, leaving it — including precompiling — entirely torules_python.
Note that runtimes provisioned by interpreters.toolchain() carry
rules_python's public PyRuntimeInfo (re-exported from
@aspect_rules_py//py:defs.bzl), so rules_python-defined executables and
their downstream consumers (py_zipapp_binary, py_interpreter) analyze and
run on them. No coverage tool is bundled, so rules_python-rule coverage on
these runtimes is unavailable.
You can migrate incrementally: replace python.toolchain() calls with
interpreters.toolchain() and remove the rules_python interpreter
configuration while keeping everything else.