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

# Virtual dependencies

> Bazel aspect_rules_py virtual deps with uv dependency-groups: resolve conflicting Python package versions in a monorepo and switch sets via a flag.

In a monorepo, different projects often need different versions of the same package. One team is ready to upgrade Django; others aren't. A library needs to be validated against the current version and the next one at the same time.

`uv` named dependency configurations are the right tool for this. By defining multiple [`[dependency-groups]`](https://docs.astral.sh/uv/concepts/dependencies/#dependency-groups) in your `pyproject.toml`, you get separate, independently resolved sets of packages that Bazel can switch between with a single flag.

## Conflicting dependency versions

When two dependency groups require incompatible versions of the same package, `uv` can resolve them independently using the [`conflicts`](https://docs.astral.sh/uv/concepts/resolution/#conflicting-dependencies) key in `pyproject.toml`. This lets both groups coexist in a single `uv.lock` without either one compromising the other.

Suppose you want to validate your library against `packaging==21.3` and `packaging==24.0` at the same time:

```toml theme={null}
# pyproject.toml
[dependency-groups]
current = [
    "packaging==21.3",
]
next = [
    "packaging==24.0",
]

[tool.uv]
conflicts = [
    [
        { group = "current" },
        { group = "next" },
    ],
]
```

`uv` resolves both groups independently and records both sets of pins in `uv.lock`. In Bazel, each group becomes a named venv. Individual targets opt into either:

```python theme={null}
# BUILD.bazel
load("@aspect_rules_py//py/unstable:defs.bzl", "py_venv_test")

py_venv_test(
    name = "test_current",
    srcs = ["test.py"],
    venv = "current",
    deps = ["@pypi//packaging"],
)

py_venv_test(
    name = "test_next",
    srcs = ["test.py"],
    venv = "next",
    deps = ["@pypi//packaging"],
)
```

Run both with a single command:

```bash theme={null}
bazel test //mylib:all
```

## Gradual monorepo upgrades

The same pattern works for managing incremental upgrades across a large codebase. Define `current` and `next` configurations, declare them as conflicting, and set `current` as the default in `.bazelrc`:

```toml theme={null}
# pyproject.toml
[dependency-groups]
current = [
    "django==3.2",
]
next = [
    "django==4.2",
]

[tool.uv]
conflicts = [
    [
        { group = "current" },
        { group = "next" },
    ],
]
```

```bash theme={null}
# .bazelrc
common --@pypi//venv=current
```

As teams migrate, they opt individual targets into the new version:

```python theme={null}
# BUILD.bazel
py_binary(
    name = "service_a",
    srcs = ["main.py"],
    deps = ["@pypi//django"],
    # Uses the default venv (current) from .bazelrc
)

py_binary(
    name = "service_b",
    srcs = ["main.py"],
    deps = ["@pypi//django"],
    venv = "next",   # This target has been migrated
)
```

Once all targets use `next`, update `.bazelrc` to point at `next`, then clean up the old group. This completes the upgrade without requiring a simultaneous migration of all targets.

## Virtual dependencies

Virtual dependencies are a low-level escape hatch for cases where named configurations don't apply. They let a `py_library` declare a dependency by name without pinning a version, leaving the resolution to each `py_binary` or `py_test` that uses the library.

Prefer named dependency configurations for monorepo-wide version management. Reach for virtual dependencies only when you need an extremely targeted override with no better option.

<Note>
  Any `py_binary` or `py_test` that transitively depends on a `py_library` with `virtual_deps` must use the `aspect_rules_py` load, not `rules_python`.
</Note>

### Step 1: Declare a virtual dependency in a library

Use `virtual_deps` instead of `deps` to declare the dependency by name only:

```python theme={null}
# my_app/BUILD.bazel
load("@aspect_rules_py//py:defs.bzl", "py_library")

py_library(
    name = "my_app",
    srcs = ["app.py"],
    # Instead of: deps = ["@pypi//django"]
    virtual_deps = ["django"],
)
```

The library now works with any version of Django. It is up to the binary to decide which one to supply.

### Step 2: Resolve the virtual dependency in a binary

Fetch a specific wheel directly using the Bazel downloader, then point the binary at it:

```python theme={null}
# MODULE.bazel
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")

http_file(
    name = "django_4_2_4",
    urls = ["https://files.pythonhosted.org/packages/7f/9e/fc6bab255ae10bc57fa2f65646eace3d5405fbb7f5678b90140052d1db0f/Django-4.2.4-py3-none-any.whl"],
    sha256 = "860ae6a138a238fc4f22c99b52f3ead982bb4b1aad8c0122bcd8c8a3a02e409d",
    downloaded_file_path = "Django-4.2.4-py3-none-any.whl",
)
```

```python theme={null}
# BUILD.bazel
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_unpacked_wheel")

py_unpacked_wheel(
    name = "django_4_2_4",
    src = "@django_4_2_4//file",
)

py_binary(
    name = "manage",
    srcs = ["manage.py"],
    deps = [":my_app"],
    resolutions = {
        "django": ":django_4_2_4",
    },
)
```

## Further reading

* [Working with multiple dependency sets](/docs/bazel/python/uv#working-with-multiple-dependency-sets) in [Manage dependencies with uv](/docs/bazel/python/uv)
* [uv conflict management](https://docs.astral.sh/uv/concepts/resolution/#conflicting-dependencies)
* [Original feature request and design discussion](https://github.com/aspect-build/rules_py/issues/213)
