Bazel Torch: When Bazel Enters Dependency Resolution

2026/02/14

BUILDbazeldependencies

Most build systems meet Python and try to play nice. Bazel meets Python and says: I don’t negotiate with non-determinism.

Bazel’s approach to Python dependency resolution is radical — it doesn’t do it. You resolve deps externally with uv or pip-compile, hand Bazel a lockfile, and Bazel downloads those exact wheels. No solver. No SAT problem. No “well, PyPI changed overnight and now CI is broken.” Just: here are the bytes, hash-verified, done.

This is not a limitation. It’s a design choice, and it’s the right one.

Why Bazel Refuses to Resolve

Dependency resolution is inherently non-deterministic. PyPI can yank packages. Multiple valid solutions exist for the same constraint set. The resolver you run today might produce different output tomorrow — not because your constraints changed, but because the universe of available packages did.

Non-determinism violates hermeticity. Hermeticity is the whole point of Bazel. So Bazel does what any sensible system does when a subproblem violates its core invariant: it outsources it.

You resolve. Bazel builds. Clean separation.

Why pip/uv but NOT Conda

pip and uv output wheels. Bazel’s pip_parse consumes wheels. That’s the bridge.

Conda outputs .conda packages and system-level binaries. There is no rules_conda. Nobody has built the bridge, and there’s a reason for that — Conda and Bazel have competing isolation models. Conda wants to own your environment. Bazel wants to own your environment. Two control freaks, one kitchen.

But what about native dependencies? CUDA, NCCL, cuDNN — the stuff Conda actually handles well?

Bazel handles native deps its own way: rules_cuda, cc_library, rules_foreign_cc. It’s more manual. It’s also more explicit. Whether that tradeoff is worth it depends on how much you enjoy writing Starlark.

Dynamic Imports Break Everything

Here’s where Python and Bazel’s worldviews truly collide.

Bazel builds a complete dependency graph at analysis time from BUILD files. Every input to every action is declared statically. If it’s not in BUILD, it doesn’t exist in the sandbox. Period.

Python’s importlib, pkg_resources, try/except imports — these decide dependencies at runtime. The graph isn’t static. It’s a choose-your-own-adventure book.

ML packages are the worst offenders. transformers conditionally imports torch, tensorflow, jax, sentencepiece, tokenizers — depending on what’s installed. torch itself does dynamic loading of CUDA libraries. None of this is visible to Bazel’s analysis phase.

So you end up over-declaring dependencies “just in case.” Every target that touches transformers gets torch, sentencepiece, tokenizers, and half of PyPI dragged in. You’re defeating Bazel’s precision to survive Python’s dynamism. It works. It’s also ugly.

rules_python vs rules_py

Two competing rulesets, two different philosophies.

rules_python (the official one) fakes site-packages with PYTHONPATH manipulation. It never creates a real virtualenv. This is fast and hermetic — packages are symlinked from Bazel’s cache, no installation step. But packages that inspect their own site-packages directory at runtime (read: most ML packages) get confused and break.

# This works fine with rules_python
import requests

# This might not
import torch  # inspects site-packages for .so files at import time

rules_py (from Aspect) creates real virtualenvs with real site-packages directories. Less pure from a hermeticity standpoint — there’s an actual pip install step. But torch finds its .so files. transformers finds its optional deps. Things just work.

For ML workloads: rules_py usually wins. Purity is nice until your model can’t load.

The Real Cost

Here’s what Bazel actually costs you:

BUILD files everywhere. Every directory. Every new Python file means updating a py_library target. Forget one dep and the sandbox kills your import at build time — which is the point, but also means your feedback loop includes “why is this import failing, oh right, BUILD file.”

CUDA wrapping is manual and brittle. You’re writing cc_import rules for .so files, pinning driver versions, managing CUDA toolkit paths. Conda does this in one line. Bazel makes you earn it.

torch is 2GB+. Bazel’s caching helps after the first build, but that first build — and every cache miss — hurts. Remote caching is almost mandatory for teams.

The learning curve is steep. Starlark isn’t hard, but the mental model is. “Everything is a target, everything is declared, nothing is implicit” takes months to internalize. Your Python engineers will resist it.

So Is It Worth It?

At Google scale, obviously. At Tesla scale, probably. At a 20-person startup — almost certainly not.

Bazel gives you hermetic, reproducible builds across a polyglot monorepo. It also gives you a second job maintaining BUILD files. The tradeoff is worth it when the cost of non-reproducibility exceeds the cost of Bazel maintenance, and that crossover point is higher than most teams think.

“At scale” means something specific. Hundreds of engineers, thousands of targets, CI that can’t afford flaky builds. If that’s you, Bazel is probably the answer. If you’re six people shipping a model — just use uv and a Dockerfile.