rules_python vs rules_py: They’’re Layers, Not Competitors

2026/03/08

BUILDbazelpython🍞

Everyone asks the same question when they find both projects: do I use rules_python or aspect_rules_py? The framing is wrong. They’re not competitors. They’re layers.

# aspect_rules_py/MODULE.bazel
bazel_dep(name = "rules_python", version = "1.0.0")

rules_py literally depends on rules_python. It doesn’t replace it – it builds on top of it. rules_python is the foundation: interpreter management, toolchain registration, the PyInfo provider protocol. rules_py is the enhanced execution layer: real virtualenvs, bash launchers, py_venv_test, IDE support.

Once you see them as layers, the “which one do I use” question dissolves into “which rules from each layer do I need.”

The full feature table

Feature rules_python rules_py Notes
Python toolchain (interpreter download) yes no python.toolchain() – only source
Local/system Python toolchain yes no local_runtime_repo for Docker/system interpreters
py_library yes yes rules_py adds virtual_deps
py_binary yes yes rules_py uses bash launcher, creates venv
py_test yes yes rules_py adds pytest_main flag
py_venv_test no yes Killer feature – system site-packages
py_venv / py_venv_binary no yes Standalone venv builder/runner
py_venv_link (auto .venv) no yes Every binary gets a .venv for IDE support
py_pex_binary no yes PEP-441 single-file deployable
py_image_layer no yes Docker layer splitting
py_pytest_main no yes Generate pytest entry points
Pytest sharding no yes Built-in Bazel test sharding
Virtual deps no yes Late-binding dep resolution
py_import yes no Import pre-built wheels
py_runtime / py_runtime_pair yes no Custom Python runtimes
py_proto_library yes no Proto to Python codegen
py_cc_toolchain yes no C/Python interop
py_package / py_wheel yes no Build distributable wheels
Precompilation (.pyc) yes no Bytecode at build time
pip.parse (requirements.txt) yes yes Both support
uv.lock resolution no yes Native lockfile reader
Gazelle plugin yes (Go src) yes (prebuilt) Same plugin, different packaging

The pattern is clear. rules_python owns the foundation – toolchains, providers, packaging, compilation. rules_py owns the execution experience – venvs, IDE support, Docker, testing ergonomics.

Why rules_py exists

rules_python grew out of Google’s internal Python rules. Google’s assumptions:

These assumptions break outside Google. Aspect Build (a Bazel consulting company) built rules_py to fix the pain points their clients kept hitting. Their README says it directly: the goal is to “behave more like idiomatic Python ecosystem tools, where rules_python is closely tied to the way Google does Python development in google3.”

The fixes are structural, not cosmetic.

The launcher problem

This is the one that surprises people.

rules_python uses a Python bootstrap script to launch the hermetic interpreter. Think about that for a second. You need Python to start Python. This is issue #691 – the bootstrap paradox. If there’s no system Python on the machine, the hermetic interpreter that rules_python carefully downloaded can’t even start.

rules_py uses a pure bash launcher instead:

# rules_py/py/private/run.tmpl.sh (simplified)
exec "{{EXEC_PYTHON_BIN}}" {{INTERPRETER_FLAGS}} "$(rlocation {{ENTRYPOINT}})" "$@"

rlocation finds the hermetic interpreter via Bazel’s runfiles, creates a venv, execs into Python. No system Python needed at any point in the chain.

This matters more than it sounds. In CI, in Docker containers, on minimal base images – anywhere you’ve carefully controlled what’s installed – having the launcher depend on system Python is a landmine.

PYTHONPATH vs real virtualenvs

rules_python puts dependencies on PYTHONPATH and manipulates sys.path. This works until it doesn’t:

rules_py creates a real virtualenv at execution time. Dependencies live in proper site-packages. Standard Python import resolution works. And every py_binary target automatically generates a companion .venv target – run bazel run //my/package:my_binary.venv and you get a virtualenv your IDE can use.

The venv approach is slower (creating a venv has overhead). The correctness and developer experience gains are worth it for most teams.

py_venv_test – the killer feature

Some dependencies don’t play nice with Bazel’s hermetic world. CUDA libraries. System-installed PyTorch. Anything that ships pre-compiled .so files tied to specific system paths.

rules_py has py_venv_test with one critical attribute:

py_venv_test(
    name = "test_with_torch",
    include_system_site_packages = True,
    tags = ["torch"],
    target_compatible_with = ["@platforms//os:linux"],
)

When include_system_site_packages = True, the test venv can see packages installed in the system Python. This is how you test code that depends on torch in Docker – system Python has torch, Bazel’s venv borrows it.

rules_python has no equivalent. I’ve grepped. It’s not there. If you need system site-packages access, rules_py is currently the only path.

Dependency resolution

rules_python uses pip.parse() with flat requirements.txt files. It works. It’s also the lowest-common-denominator format – no cross-platform resolution markers, no distinction between sdists and wheels, no lock integrity verification.

rules_py has a uv extension that reads uv.lock directly. The uv.lock format is richer: per-platform resolution data, cross-platform markers, sdist build support. If your project already uses uv (and it probably should), this is a natural fit.

Both support requirements.txt. But if you’re starting fresh, the uv.lock path gives you better reproducibility with less effort.

What each has that the other doesn’t

Only in rules_python (can’t get from rules_py):

Only in rules_py (can’t get from rules_python):

When to use which

Scenario Use Why
Need a Python interpreter rules_python Only option
Need system Python as toolchain rules_python local_runtime_repo
py_library / py_binary / py_test rules_py Better execution model
Need system site-packages (torch, CUDA) rules_py py_venv_test – only option
Managing deps via uv.lock rules_py uv extension
Managing deps via requirements.txt rules_python pip.parse()
Building Docker images from Python targets rules_py py_image_layer
Building distributable wheels rules_python py_wheel
Proto to Python codegen rules_python py_proto_library
IDE integration rules_py Auto .venv targets

The realistic answer for most projects: use both. rules_python for the toolchain and anything it exclusively provides. rules_py for the day-to-day rules you actually write in BUILD files. They compose cleanly – that’s the whole point.

The bottom line

rules_python is the foundation you can’t avoid. rules_py is the execution layer you’ll probably want. They’re designed to work together, and trying to pick one “instead of” the other misunderstands the relationship.

Start with rules_python for your toolchain. Add rules_py when you hit the first pain point – PYTHONPATH collisions, missing IDE support, needing system site-packages. You’ll hit it faster than you think.