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’s Python rules use uv as the recommended package manager, which replaces the traditional pip workflow. Where pip reads a flat requirements.txt, uv reads a uv.lock lockfile that captures the full dependency graph that includes every package, every version, and every download checksum in a single file that works on any platform.
The uv integration is currently experimental. The API is stable enough for production use, but some advanced features may change between releases. Load it from @aspect_rules_py//uv/unstable:extension.bzl.

Why uv instead of pip?

pip / rules_pythonuv
Lockfile formatrequirements.txt (flat list)uv.lock (full dependency graph)
Cross-platformSeparate file per OSSingle file, all platforms
Cross-platform container buildsNot supportedBuild Linux images from Mac
Build speedSlow (runs at Bazel startup)Fast (lazy, runs only when needed)
Dependency cyclesManual resolution requiredAutomatic
Private package registriesLimitedFull support

How uv works

uv introduces the concept of a hub—a central Bazel repository that provides all your Python packages at addresses like @pypi//requests or @pypi//flask. Under the hood, the hub is backed by one or more venvs, or virtual environments, each representing a different set of packages. A Bazel flag controls which venv is active for a given build.
@pypi                              # The hub repository
@pypi//requests:requests           # The requests library
@pypi//requests:whl                # The wheel file for requests
@pypi//jinja2-cli/entrypoints:jinja2-cli   # An entrypoint declared by a package

Setup

Step 1: Generate a uv.lock

If you already have a pyproject.toml, run:
uv lock
If you’re migrating from an existing requirements.txt, uv can convert it for you. This script creates a temporary project, imports your requirements, and generates the lockfile:
d=$(mktemp -d)
cat <<'EOF' > $d/pyproject.toml
[project]
name = "dummy"
version = "0.0.0"
requires-python = ">= 3.9"
dependencies = []
EOF
cp requirements_lock.txt $d/
(
  cd $d
  uv add -r requirements_lock.txt
  uv lock
)
cp $d/uv.lock .
rm -r $d

Step 2: Configure MODULE.bazel

Add the uv extension to your MODULE.bazel and declare a hub:
# MODULE.bazel
bazel_dep(name = "aspect_rules_py", version = "1.6.7") # or later

uv = use_extension("@aspect_rules_py//uv/unstable:extension.bzl", "uv")

# Declare a hub named "pypi" (you can choose any name)
uv.declare_hub(
    hub_name = "pypi",
)

# Point the hub at your pyproject.toml and uv.lock
uv.project(
    hub_name = "pypi",
    pyproject = "//:pyproject.toml",
    lock = "//:uv.lock",
)

use_repo(uv, "pypi")
Each [dependency-group] defined in your pyproject.toml becomes a named venv configuration. If you have no dependency groups, rules_py creates an implicit default using the project name.

Step 3: Set a default venv in .bazelrc

Add a line to .bazelrc to tell Bazel which venv to use by default. Replace myproject with the name from your pyproject.toml:
# .bazelrc
common --@pypi//venv=myproject

Step 4: Use packages in BUILD files

Reference packages from the hub in your py_binary, py_library, or py_test targets:
# BUILD.bazel
load("@aspect_rules_py//py:defs.bzl", "py_binary")

py_binary(
    name = "app",
    srcs = ["__main__.py"],
    deps = ["@pypi//requests"],
)

Working with multiple dependency sets

If your project has different dependency sets; for example, one for production and one for development or testing, you can register them as separate venvs within the same hub. Define them as [dependency-group] entries in your pyproject.toml:
[dependency-groups]
default = ["flask", "requests"]
dev = ["flask", "requests", "pytest", "ruff"]
Individual targets can opt into a specific venv using the venv attribute:
# BUILD.bazel
py_binary(
    name = "app",
    srcs = ["__main__.py"],
    deps = ["@pypi//flask"],
    # Uses the default venv from .bazelrc
)

py_test(
    name = "app_test",
    srcs = ["app_test.py"],
    deps = ["@pypi//pytest"],
    venv = "dev",    # Explicitly use the dev venv
)
Switch the active venv for a whole build with a flag:
bazel test //... --@pypi//venv=dev

Cross-platform builds

uv can build packages for a different operating system or CPU architecture than the machine you’re building on; for example, producing a Linux/ARM64 container image from a Mac. This makes docker run your app possible without a Linux machine.

Define a target platform

Define a platform() target that describes the machine you’re building for. The platform_libc and platform_version flags tell uv which version of the C standard library is available on that target, which is important for packages that include compiled C code:
platform(
    name = "arm64_linux",
    constraint_values = [
        "@platforms//os:linux",
        "@platforms//cpu:aarch64",
    ],
    flags = [
        # glibc 2.39 is common on recent Ubuntu/Debian
        "--@aspect_rules_py//uv/private/constraints/platform:platform_libc=glibc",
        "--@aspect_rules_py//uv/private/constraints/platform:platform_version=2.39",
    ],
)
Common libc values: glibc for Ubuntu, Debian, and most Linux systems; musl for Alpine Linux; and libsystem for macOS. Check your target system’s docs if you’re unsure.

Build a container image

Use py_image_layer and platform_transition_filegroup to produce a container image layer set built for your target platform:
load("@bazel_lib//lib:transitions.bzl", "platform_transition_filegroup")
load("@aspect_rules_py//py/unstable:defs.bzl", "py_venv_binary")
load("@aspect_rules_py//py:defs.bzl", "py_image_layer")

py_venv_binary(
    name = "app_bin",
    srcs = ["__main__.py"],
    main = "__main__.py",
    python_version = "3.12",
    venv = "default",
    deps = ["@pypi//flask"],
)

# Package the binary and its deps into OCI image layers
py_image_layer(
    name = "app_layers",
    binary = ":app_bin",
)

# Rebuild those layers for ARM64 Linux
platform_transition_filegroup(
    name = "arm64_layers",
    srcs = [":app_layers"],
    target_platform = ":arm64_linux",
)

Swapping in a local package

Use uv.override_package in MODULE.bazel to replace a downloaded package with a local Bazel target. This is useful for vendoring, patching, or iterating on a fork of a dependency. For example:
# MODULE.bazel
uv.override_package(
    lock = "//:uv.lock",
    name = "cowsay",
    target = "//third_party/py/cowsay:cowsay",
)
Any target that depends on @pypi//cowsay now gets your local version instead of the one from PyPI.

Constraining library compatibility

By default, a py_library target works with any venv that provides its required packages. But sometimes you need to prevent a library from being used with a particular venv. For example, this is useful during a migration or when the library requires a package version unavailable in that venv. Use the compatible_with and incompatible_with helpers from your hub’s defs.bzl:
load("@pypi//:defs.bzl", "compatible_with", "incompatible_with")

# This library only works with the "dev" venv
py_library(
    name = "dev_only_lib",
    srcs = ["foo.py"],
    deps = ["@pypi//pytest"],
    target_compatible_with = select(compatible_with(["dev"])),
)

# This library explicitly does NOT work with the "legacy" venv
py_library(
    name = "modern_lib",
    srcs = ["bar.py"],
    deps = ["@pypi//flask"],
    target_compatible_with = select(incompatible_with(["legacy"])),
)
Bazel produces a clear error if someone tries to build an incompatible target in the wrong venv configuration to help catch mistakes at build time rather than runtime.

Best practices

Use one hub. You can name the hub anything you like, often pypi to match the old pip.parse convention, but keep just one per repository. Different dependency sets belong as separate venvs inside the same hub, not as separate hubs. Multiple hubs lead to version conflicts where two parts of your build use different versions of the same package. Set your default venv in .bazelrc. This keeps the scope of the default well-bounded to your repository. Don’t set defaults in MODULE.bazel. Keep setuptools and build in your lockfile. uv needs these Python build tools to compile packages from source, called sdists. If they’re missing and any package requires a source build, you’ll get a configuration error. Just add them to your pyproject.toml dependencies to be safe.

Tips

Entrypoints aren’t auto-detected. Tools like ruff or black that install command-line scripts need their entrypoints manually declared as Bazel targets. uv installs packages during the build rather than at setup time, so it can’t inspect installed files the way pip does. Annotations for sdist builds. The uv.lock format doesn’t record what packages are needed specifically for building other packages from source. If you’re hitting errors with sdist builds, annotations are the workaround. No default venv error. If Bazel flags a missing venv, make sure you’ve added common --@pypi//venv=<name> to your .bazelrc.