Documentation Index
Fetch the complete documentation index at: https://docs.aspect.build/llms.txt
Use this file to discover all available pages before exploring further.
The interpreter provisioning feature is available under py/unstable and is a preview API. No compatibility guarantees are made across releases.
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—no
repinning or manifest regeneration required.
No editorial decisions. Any PBS release exposes every version it contains.
Need Python 3.8? Add an older release date that includes it.
Windows and cross-platform support. Nine platforms are registered out of the box, including Windows on x86_64, aarch64, and i686, Linux with glibc and musl, and macOS.
Quickstart
Add the following to your MODULE.bazel:
bazel_dep(name = "aspect_rules_py", version = "1.9.0")
interpreters = use_extension("@aspect_rules_py//py/unstable:extension.bzl", "python_interpreters")
interpreters.toolchain(
is_default = True,
python_version = "3.12",
)
interpreters.toolchain(python_version = "3.11")
use_repo(interpreters, "python_interpreters")
register_toolchains("@python_interpreters//:all")
The extension uses a set of default PBS release dates covering Python 3.8
through 3.15. The extension automatically selects the newest available build for each requested version.
Configuring releases
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/unstable:extension.bzl", "python_interpreters")
interpreters.configure(
releases = ["20260303", "20241002"],
)
interpreters.toolchain(python_version = "3.12", is_default = True)
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 extension prefers the newest release. The module graph accepts only one configure() tag, and only the root module’s tag takes effect. Dependency modules may include configure() without error, but the extension silently ignores it.
Using the latest release
For development workflows where you always want the newest PBS release:
interpreters.configure(releases = ["latest"])
This resolves to the newest release using the GitHub releases API. Because the result depends on when it runs, this configuration marks the extension as non-reproducible—Bazel re-evaluates it on each invocation rather than caching it.
For CI and production builds, prefer explicit release dates.
Using a mirror or fork
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.
Module scoping
Only the root module’s configure() tag and is_default and pre_release flags on toolchain() take effect. This gives the root module full control over the build environment while allowing dependency modules to declare which Python versions they need.
The following table shows which settings each module type controls:
| 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(is_default = True) | Honored | Silently ignored |
toolchain(pre_release = True) | Honored | Silently ignored |
If a dependency module requests a Python version that no root-module release includes, the build fails with a clear error message identifying which module requested it.
Selecting Python versions
There are three layers of version control, from broadest to most specific.
Default version (MODULE.bazel)
Bazel selects the is_default = True toolchain when nothing else overrides it:
interpreters.toolchain(python_version = "3.12", is_default = True)
Build-wide default (.bazelrc)
Set --@aspect_rules_py//py:python_version in .bazelrc to lock the entire
build to a specific version. This is good practice even when it matches the
is_default toolchain—it makes the choice explicit and visible in version
control:
# .bazelrc
common --@aspect_rules_py//py:python_version=3.12
You can also override this flag on the command line for one-off testing:
bazel test //... --@aspect_rules_py//py:python_version=3.11
Per-target overrides (python_version attribute)
Individual py_binary, py_venv_binary, py_test, and py_venv_test targets
can pin to a specific version using the python_version attribute, which 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"]]
Precedence
The most specific setting wins: python_version attribute > flag > default toolchain.
| Mechanism | Scope | Set by |
|---|
is_default = True on toolchain() | Whole build (fallback) | MODULE.bazel |
--@aspect_rules_py//py:python_version | Whole build | .bazelrc or command line |
python_version attribute | Single target | BUILD.bazel |
Build configurations
PBS provides several build configurations that control how the Python interpreter
itself is compiled. The right choice depends on your priorities: binary size,
debug support, or experimental features like free-threading.
The default is install_only, which uses PGO+LTO compilation on platforms that
support it. PGO (Profile-Guided Optimization) and LTO (Link-Time Optimization)
produce a faster interpreter by letting the compiler optimize across the entire
program using real runtime behavior. This configuration suits most use cases.
Use install_only_stripped to reduce binary size by removing debug symbols,
which is useful in environments where disk space or distribution size matters.
The freethreaded configurations disable the GIL (Global Interpreter Lock),
allowing true parallel execution across threads. Free-threading is experimental
and was introduced in Python 3.13. Use these configurations only if you are
explicitly targeting the free-threaded build and your dependencies support it.
Select a configuration per toolchain using the build_config attribute:
interpreters.toolchain(
python_version = "3.12",
build_config = "install_only_stripped",
)
| Configuration | Description |
|---|
install_only | Standard optimized build (default) |
install_only_stripped | Same as install_only but with debug symbols stripped |
freethreaded+pgo+lto | Free-threaded (no GIL) build with PGO+LTO optimization |
freethreaded+debug | Free-threaded debug build |
These platforms are available 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. The toolchain resolver silently skips unavailable combinations.
Compatibility with rules_python
This interpreter provisioning is designed to coexist with rules_python:
- Toolchain registration uses the standard
@bazel_tools//tools/python:toolchain_type,
so these interpreters work with all existing Python rules.
- Build transitions keep the
@rules_python//python/config_settings:python_version flag in
sync with aspect_rules_py’s own version flag.
aspect_rules_py uses py_runtime and py_runtime_pair from rules_python to create
the runtime providers.
You can migrate incrementally: replace python.toolchain() calls with
interpreters.toolchain() and remove the rules_python interpreter
configuration while keeping everything else unchanged.