> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage dependencies with uv

> Use uv with Bazel aspect_rules_py for fast Python dependency management: cross-platform uv.lock lockfiles, container builds, and lazy resolution.

Aspect's Python rules use [uv](https://github.com/astral-sh/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.

<Note>
  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`.
</Note>

## Why uv instead of pip?

|                                 | pip / rules\_python            | uv                                 |
| ------------------------------- | ------------------------------ | ---------------------------------- |
| Lockfile format                 | `requirements.txt` (flat list) | `uv.lock` (full dependency graph)  |
| Cross-platform                  | Separate file per OS           | Single file, all platforms         |
| Cross-platform container builds | Not supported                  | Build Linux images from Mac        |
| Build speed                     | Slow (runs at Bazel startup)   | Fast (lazy, runs only when needed) |
| Dependency cycles               | Manual resolution required     | Automatic                          |
| Private package registries      | Limited                        | Full 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:

```bash theme={null}
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:

```bash theme={null}
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:

```python theme={null}
# 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`:

```bash theme={null}
# .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:

```python theme={null}
# 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`:

```toml theme={null}
[dependency-groups]
default = ["flask", "requests"]
dev = ["flask", "requests", "pytest", "ruff"]
```

Individual targets can opt into a specific venv using the `venv` attribute:

```python theme={null}
# 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:

```bash theme={null}
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:

```python theme={null}
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",
    ],
)
```

<Tip>
  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.
</Tip>

### Build a container image

Use `py_image_layer` and `platform_transition_filegroup` to produce a
container image layer set built for your target platform:

```python theme={null}
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:

```python theme={null}
# 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`:

```python theme={null}
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`.
