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:
- System Python is available for bootstrapping
PYTHONPATHmanipulation is fine (no naming collisions in google3)- pip +
requirements.txtis sufficient for dependency resolution
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:
- Name collisions with stdlib – your dep named
typingshadows the stdlib one - System packages leak in – anything installed system-wide is visible (#27)
importlib.metadatabreaks – because packages aren’t in a realsite-packages- No IDE support – your editor can’t resolve imports because there’s no venv to point at (#1401)
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):
- Python toolchain – interpreter download and registration
local_runtime_repo– system/Docker Python as toolchainPyInfo/PyRuntimeInfoproviders – the interop protocolpy_proto_library,py_cc_toolchain– codegen and C interoppy_package/py_wheel– build distributable packages- Precompilation –
.pyto.pycat build time py_runtime/py_runtime_pair– custom runtime definitions
Only in rules_py (can’t get from rules_python):
py_venv_testwithinclude_system_site_packages- Auto
.venvper target for IDE support py_pex_binary– single-file deployable binariespy_image_layer– Docker image layer splitting- Pytest sharding – built-in Bazel test sharding
- Virtual deps – late-binding dependency resolution
- uv.lock native resolution
- Bash launcher – no bootstrap paradox
- Venv-based execution – proper
site-packages
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.