aspect_rules_py is a high-performance alternative to rules_python, the
reference Python ruleset for Bazel.
It provides drop-in replacements for py_binary, py_library, and py_test that prioritize:
- Blazing-fast dependency resolution via native
uvintegration - Strict hermeticity with isolated Python execution and Bash-based launchers
- Idiomatic Python layouts using standard
site-packagessymlink trees - Seamless IDE compatibility via virtualenv-native structures
- Production-ready containers with optimized OCI image layers
Unlike rules_python, which maintains strict compatibility with Google's internal monorepo semantics (google3),
aspect_rules_py optimizes for modern Python development workflows, large-scale monorepos, and Remote Build Execution (
RBE) environments.
| Feature | rules_python | rules_py |
|---|---|---|
| Dependency resolution | pip.parse (repo rules, loading phase) |
Build-action wheel installs (whl_install) |
| uv integration | uv pip compile → requirements.txt → pip.parse |
Native uv.lock consumption |
| Cross-platform lockfile | Per-platform requirements_*.txt |
Single uv.lock for all platforms |
| sdist / PEP 517 builds | Not supported (#2410, open since Nov 2024) | Build actions (pep517_whl, pep517_native_whl) |
| Interpreter provisioning | Download via rules_python extension | Own python-build-standalone extension — no rules_python required |
| Site-packages layout | sys.path / PYTHONPATH manipulation |
Standard site-packages symlink tree |
| Cross-compilation | Limited | Native platform transitions (e.g. arm64 image on amd64 host) |
| Virtual dependencies | No | virtual_deps — swap implementations at binary level |
| PEP 735 dependency groups | No | --@pypi//venv=prod flag |
Note
rules_python's uv support: rules_python's uv integration runs uv pip compile as a build action to
generate a requirements.txt—it is a faster pip-compile replacement. The result still feeds into pip.parse() →
whl_library repository rules at loading phase. There is no uv.lock consumption; the rules_python maintainer has
suggested this work belongs in a dedicated project.
Instead of relying on legacy pip machinery, we provide native integration with uv,
a Rust-native Python package resolver.
- Build-action installs: Wheel extraction runs as Bazel execution-phase actions—not repo rules—so they are
cacheable, sandboxed, and compatible with RBE. Crucially, wheels are no longer resolved against the host machine
architecture: a single build can fetch and extract wheels for any exec or target platform, enabling true
cross-platform builds (e.g. building Linux
aarch64wheels on a macOSx86_64host) - Native
uv.lockparsing: Consumesuv.lockdirectly; norequirements.txtgeneration step - Universal lockfiles: A single
uv.lockworks across all platforms—no morerequirements_linux.txt,requirements_mac.txt,requirements_windows.txt - sdist / PEP 517 builds: Build source distributions as Bazel actions (rules_python has no equivalent; #2410 open since November 2024)
- PEP 735 dependency groups: Define
prod,dev,testdependency groups and switch between them with a flag - Editable requirements: Override locked packages with local
py_librarytargets viauv.override_package() - Lazy downloads: All fetching happens during the build phase, not repository loading—fully compatible with private mirrors and RBE
aspect_rules_py ships its own python-build-standalone
interpreter extension—rules_python is not required as a toolchain provider.
Note
The //py/unstable and //uv/unstable extension paths indicate that the extension API may evolve across
releases—not that the features are unstable.
interpreters = use_extension("@aspect_rules_py//py/unstable:extension.bzl", "python_interpreters")
interpreters.toolchain(python_version = "3.12", is_default = True)
use_repo(interpreters, "python_interpreters")
register_toolchains("@python_interpreters//:all")This enables cross-compilation from any host to any target without host-installed Python, and is the foundation for correct toolchain selection in RBE environments.
We do not manipulate sys.path or $PYTHONPATH. Instead, we generate a standard site-packages directory structure
using symlink trees:
- Prevents module name collisions (e.g., standard library
collectionsvs. a transitive dependency namedcollections) - Matches standard Python expectations—tools just work
- Native IDE compatibility: VSCode, PyCharm, and language servers resolve jump-to-definition correctly into the Bazel sandbox
- Isolated mode: Python executes with
-Iflag, preventing implicit loading of user site-packages or host environment variables - Hermetic launchers: Our launcher uses the Bazel Bash toolchain, not the host Python, this ensures 100% hermetic execution across local machines and RBE nodes
- No host Python leakage: Breaks the implicit dependency on system Python during the boot sequence
- Effortless cross-compilation: Build Linux container images from macOS (or vice versa) using standard Bazel platform transitions
- Multi-architecture OCI images: Native support for building
amd64andarm64container images - Platform-agnostic queries: All hub labels are always available—no more "target incompatible" errors when querying on a different OS
virtual_deps allow external Python dependencies to be specified by package name rather than by label:
- Individual projects within a monorepo can upgrade dependencies independently
- Test against multiple versions of the same dependency
- Swap implementations at the binary level (e.g., use
cowsnakeinstead ofcowsay)
Built-in rules for creating optimized container images:
py_image_layer: Creates layered tar files compatible withrules_oci- Cross-platform builds with automatic platform transitions
- Optimized layer caching—dependencies and application code are separated
- First-class pytest support with
py_pytest_main - Automatic test discovery with proper import handling
- Compatible with
pytest-mock,pytest-xdist, and other plugins
bazel_dep(name = "aspect_rules_py", version = "1.11.2")Load rules from aspect_rules_py in your BUILD files:
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_library", "py_test")
py_library(
name = "lib",
srcs = ["lib.py"],
deps = ["@pypi//requests"],
)
py_binary(
name = "app",
srcs = ["main.py"],
main = "main.py",
deps = [":lib"],
)
py_test(
name = "test",
srcs = ["test.py"],
deps = [":lib"],
)aspect_rules_py//uv is our alternative to rules_python's pip.parse:
uv = use_extension("@aspect_rules_py//uv/unstable:extension.bzl", "uv")
# 1. Declare a hub (a shared dependency namespace)
uv.declare_hub(
hub_name = "pypi",
)
# 2. Register projects (lockfiles) into the hub
uv.project(
hub_name = "pypi",
lock = "//:uv.lock",
pyproject = "//:pyproject.toml",
# Build tools injected for sdist packages that need them (e.g. maturin, setuptools)
default_build_dependencies = ["build", "setuptools"],
)
# 3a. (Optional) Replace a package with a local Bazel target
uv.override_package(
name = "some_package",
lock = "//:uv.lock",
target = "//third_party/some_package",
)
# 3b. (Optional) Patch an installed wheel's file tree after unpacking
uv.override_package(
name = "some_other_package",
lock = "//:uv.lock",
post_install_patches = ["//third_party/patches:fix_some_other_package.patch"],
post_install_patch_strip = 1,
)
use_repo(uv, "pypi")Requirements are declared in standard pyproject.toml:
[project]
name = "myapp"
version = "1.0.0"
requires-python = ">= 3.11"
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
[dependency-groups]
dev = ["pytest", "black", "mypy"]Generate the lockfile with uv:
uv lockSwitch between dependency groups:
# Default: use all dependencies
bazel run //:app
# Use only production dependencies
bazel run //:app --@pypi//venv=prodDeclare virtual dependencies in libraries:
py_library(
name = "greet_lib",
srcs = ["greet.py"],
virtual_deps = ["cowsay"], # Not a label—just a package name
)Resolve them in binaries:
py_binary(
name = "app",
srcs = ["main.py"],
deps = [":greet_lib"],
resolutions = {
"cowsay": "@pypi//cowsay",
},
)
# Or use a different implementation!
py_binary(
name = "app_snake",
srcs = ["main.py"],
deps = [":greet_lib"],
resolutions = {
"cowsay": "//cowsnake", # Swapped implementation
},
)Build optimized OCI images with layer caching:
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_image_layer")
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load")
py_binary(
name = "app_bin",
srcs = ["main.py"],
deps = ["//:lib"],
)
py_image_layer(
name = "app_layers",
binary = ":app_bin",
)
oci_image(
name = "image",
base = "@ubuntu",
tars = [":app_layers"],
entrypoint = ["/app/app_bin"],
)
oci_load(
name = "image_load",
image = ":image",
repo_tags = ["myapp:latest"],
)Cross-compile for Linux from macOS:
bazel build //:image --platforms=//platforms:linux_amd64aspect_rules_py generates standard virtualenv structures that IDEs understand:
# Creates a .venv symlink for the target
bazel run //:my_app.venvThen point your IDE to the generated virtualenv:
- VSCode: Set
python.defaultInterpreterPathto the.venvpath - PyCharm: Add the
.venvas a Python interpreter - Neovim/LSP: Configure
python-lsp-serverorpyrightto use the virtualenv
Attach debuggers using debugpy:
# In debug mode, wraps the binary with debugpy listener
py_binary(
name = "app_debug",
srcs = ["main.py"],
deps = ["//:lib", "@pypi//debugpy"],
env = {"DEBUGPY_WAIT": "1"}, # Wait for IDE attachment
)VSCode launch.json:
{
"name": "Attach to Bazel py_binary",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 5678
}
}Generate BUILD files automatically with the Gazelle extension:
# MODULE.bazel
bazel_dep(name = "gazelle", version = "0.42.0")
bazel_dep(name = "aspect_rules_py", version = "1.11.2")
# In your BUILD file
# gazelle:map_kind py_library py_library @aspect_rules_py//py:defs.bzl
# gazelle:map_kind py_binary py_binary @aspect_rules_py//py:defs.bzl
# gazelle:map_kind py_test py_test @aspect_rules_py//py:defs.bzl# Generate BUILD files
bazel run //:gazelleaspect_rules_py is designed for incremental adoption:
- Swap the rules: Load
py_binary,py_library,py_testfrom@aspect_rules_py//py:defs.bzlinstead of@rules_python//python:defs.bzl - Migrate dependencies: Replace
pip.parsewithuv.huband generate auv.lock - Optionally migrate toolchains: Replace
rules_pythoninterpreter provisioning with theaspect_rules_pyinterpreter extension for fully independent hermetic interpreters
For detailed migration guidance, see docs/migrating.md.
- Dependency resolution with
uv - Virtual dependencies
- Interpreter configuration
- Migration guide
- Contributing
- OpenAI
- Physical Intelligence
- RAI Institute
- NVIDIA OSMO
- ZML
- Eclipse SCORE
- Intrinsic
- Enfabrica
- ReSim AI
- StackAV
- Netherlands Cancer Institute
- pyrovelocity
| Layer | Implementation | Description |
|---|---|---|
| Toolchains | @aspect_rules_py//py |
Own python-build-standalone interpreter provisioning; @rules_python optional |
| Resolution | @aspect_rules_py//uv |
Fast, lockfile-backed dependency resolution with uv |
| Execution | @aspect_rules_py//py |
Drop-in replacements for py_binary, py_library, py_test with sandbox isolation |
| Generation | aspect-gazelle |
Pre-compiled Gazelle extension—no CGO toolchain required |
Apache 2.0 - see LICENSE