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.

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] 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 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:
# 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:
# 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:
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:
# pyproject.toml
[dependency-groups]
current = [
    "django==3.2",
]
next = [
    "django==4.2",
]

[tool.uv]
conflicts = [
    [
        { group = "current" },
        { group = "next" },
    ],
]
# .bazelrc
common --@pypi//venv=current
As teams migrate, they opt individual targets into the new version:
# 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.
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.

Step 1: Declare a virtual dependency in a library

Use virtual_deps instead of deps to declare the dependency by name only:
# 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:
# 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",
)
# 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