Skip to main content
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?

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.

Setup

Step 1: Generate a uv.lock

If you already have a pyproject.toml, run:
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:

Step 2: Configure MODULE.bazel

Add the uv extension to your MODULE.bazel and declare a hub:
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:

Step 4: Use packages in BUILD files

Reference packages from the hub in your py_binary, py_library, or py_test targets:

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:
Individual targets can opt into a specific venv using the venv attribute:
Switch the active venv for a whole build with a flag:

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:
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:

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:
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:
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.