Skip to main content

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.

aspect_rules_py is a near drop-in replacement for rules_python. The same rule names of py_binary, py_library, and py_test work with the same attributes, so most projects can migrate with only a few targeted changes. Switching to uv for dependency management is optional but unlocks cross-platform lockfiles, faster builds, and container image support.

The four layers of a Python build

A Bazel Python build has four layers. This table shows what handles each one in the legacy rules_python versus the Aspect-recommended approach:
LayerLegacyRecommended
Interpreter: fetch a hermetic Python binaryrules_pythonaspect_rules_py
Dependencies: install packages from PyPIrules_python + pipaspect_rules_py + uv
Rules: py_binary, py_library, py_testrules_pythonaspect_rules_py
Gazelle: auto-generate BUILD filesrules_pythonaspect-gazelle
aspect_rules_py now provides its own interpreter provisioning. All four layers are where aspect_rules_py makes significant improvements.

Step 1: Add aspect_rules_py to MODULE.bazel

Add aspect_rules_py as a dependency alongside your existing rules_python:
# MODULE.bazel
bazel_dep(name = "aspect_rules_py") # add this

Step 2: Update load statements

The minimal migration involves replacing load() statements in your BUILD files. The rule names stay the same, only the source changes:
# Before
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")

# After
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_library", "py_test")
The rule attributes, including srcs, deps, data, and args, stay exactly the same.

Step 3: Update Gazelle if you use it

If you use Gazelle to auto-generate BUILD files, add these directives to any ancestor BUILD file of your Python code. They tell Gazelle to write aspect_rules_py load() statements instead of rules_python ones going forward:
# 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
Then run Gazelle to regenerate all affected BUILD files at once:
bazel run gazelle

Step 4: Migrate to uv

Aspect highly recommends this optional step. uv replaces pip.parse as the dependency management layer, which gives you a single cross-platform lockfile, faster builds, and support for cross-platform container builds.

Before: pip.parse

# MODULE.bazel
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
    hub_name = "my_deps",
    python_version = "3.11",
    requirements_lock = "//:requirements_lock_3_11.txt",
)
use_repo(pip, "my_deps")

After: uv

First, if you don’t already have a pyproject.toml, create one:
uv init --no-workspace
Then, import your existing dependencies using the lockfile as constraints so uv produces the same dependency solution you already have:
uv add -r requirements.txt -c requirements_lock.txt
Then replace the pip.parse block in MODULE.bazel with:
# MODULE.bazel
uv = use_extension("@aspect_rules_py//uv/unstable:extension.bzl", "uv")
uv.declare_hub(hub_name = "pypi")
uv.project(
    hub_name = "pypi",
    pyproject = "//:pyproject.toml",
    lock = "//:uv.lock",
)
use_repo(uv, "pypi")
Add a default venv to .bazelrc. Use the name from your pyproject.toml:
# .bazelrc
common --@pypi//venv=myproject
Package references in BUILD files change from @my_deps//... to @pypi//...:
# Before
deps = ["@my_deps//requests"]

# After
deps = ["@pypi//requests"]
Each [dependency-group] in your pyproject.toml becomes a named build configuration. This is a significant improvement over pip.parse, which required a separate requirements_lock.txt per Python version and had no concept of switching dependency sets at build time. With uv, you can switch between configurations — production, dev, testing — with a single flag:
bazel test //... --@pypi//venv=dev
Aspect’s uv integration also creates an implicit default configuration named after your project, so you don’t need to define one explicitly for the common case. For full setup and configuration details, see Manage dependencies with uv.

Features available after migrating

aspect_rules_py adds capabilities that rules_python does not support:
  • per-target Python version pinning
  • cross-platform container builds
  • IDE support through real virtualenvs

Pin a specific Python version per target

Bazel normally uses a single Python version across the entire build, set globally in MODULE.bazel. In a monorepo, this becomes an issue when, for example, different teams are on different versions, or when you need to validate that a library still works on an older runtime before upgrading. aspect_rules_py adds a python_version attribute to py_binary and py_test, so each target can declare the version it needs independently of everything else in the repository.
py_binary(
    name = "app",
    srcs = ["say.py"],
    python_version = "3.9",   # This target uses 3.9
    deps = ["@pypi//cowsay"],
)

py_test(
    name = "app_test",
    srcs = ["app_test.py"],
    python_version = "3.9",
    deps = ["@pypi//pytest"],
)

Swap in a local or patched package

To replace a PyPI package with a local Bazel target for vendoring, patching, or iterating on a fork, use uv.override_package in MODULE.bazel:
# MODULE.bazel
uv.override_package(
    lock = "//:uv.lock",
    name = "cowsay",
    target = "//third_party/py/cowsay:cowsay",
)
Any target that depends on @pypi//cowsay uses your local version instead of the one from PyPI. See Swapping in a local package in the uv guide for details.

Cross-platform container builds

A rules_python limitation rules_python cannot produce correct Linux container images from a Mac. When pip installs packages, it selects wheels for the host platform. On a Mac, that means macOS wheels: compiled extensions linked against macOS system libraries, with .dylib references and platform-specific binaries. When those artifacts are packaged into a container and run on Linux, the result is a crash or silent misbehavior. This is a fundamental architectural limitation of rules_python, not a configuration problem. How aspect_rules_py solves it aspect_rules_py with uv was built specifically to solve this. uv selects wheels for the target platform regardless of where the build is running. Declare a target platform, and Bazel fetches the correct Linux wheels, links against the correct Linux system libraries, and produces an image that works on Linux from your Mac, without Docker installed.
platform(
    name = "amd64_linux",
    constraint_values = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
    ],
)
bazel build //app:image --platforms=:amd64_linux
For the full setup including ARM64 and libc version configuration, see Cross-platform builds in Manage dependencies with uv.

IDE support out of the box

aspect_rules_py creates a real Python virtualenv for each build. PyCharm, VS Code, and other editors can discover it automatically, which gives you working autocomplete and jump-to-definition without any extra IDE configuration.

Compatibility notes

rules_python and aspect_rules_py can coexist. You don’t have to migrate everything at once. You can load py_binary from aspect_rules_py in one package while another package still uses rules_python. Migrate incrementally as it suits your team. Entrypoints require manual declaration. With pip.parse, such as command-line scripts installed by packages like ruff or black, pip detects entrypoints automatically at setup time. With uv, installs happen during the build, so you must declare entrypoints as Bazel targets if you need to run them directly. setuptools and build must be in your lockfile. uv needs these tools to compile packages that don’t have pre-built wheels. Add them to your pyproject.toml to avoid configuration errors:
[project]
dependencies = [
  "setuptools",
  "build",
  # ... your other deps
]