Why Bazel is Revolutionary for Python Development
Before diving into code, let's understand why Google open-sourced their internal build system and why it's becoming essential for serious Python development.
The Problem with Traditional Python Build Systems
Most Python developers are familiar with this workflow:
pip install -r requirements.txt
python setup.py build
python -m pytest
This approach breaks down at scale:
- Inconsistent builds: "Works on my machine" syndrome
- Slow rebuilds: Everything rebuilds even when only one file changes
- Dependency hell: Version conflicts across projects
- No parallelization: Tests and builds run sequentially
- Language barriers: Hard to integrate C++, Java, or other languages
How Bazel Solves These Problems
Bazel introduces several revolutionary concepts:
Hermetic Builds: Same inputs always produce identical outputs, regardless of the machine or environment.
Incremental Builds: Only rebuilds what actually changed, using cryptographic hashing to detect changes.
Massive Parallelization: Builds independent targets simultaneously across multiple cores.
Remote Caching: Share build artifacts across your entire team or CI/CD pipeline.
Multi-language Support: Python, Java, C++, Go, and more in a single build system.
Why MODULE.bazel Over WORKSPACE
Bazel is transitioning from WORKSPACE files to MODULE.bazel (called "Bzlmod") for several reasons:
- Better dependency resolution: Handles version conflicts automatically
- Simplified syntax: Less boilerplate, more intuitive
- Improved performance: Faster loading and resolution
- Future-proof: This is where Bazel is heading
Now let's build our first modern Bazel Python project!
Setting Up Your First Modern Bazel Python Project
Project Structure Overview
We'll create a minimal but complete Bazel project:
bazel-python-tutorial/
├── MODULE.bazel # Modern dependency management (replaces WORKSPACE)
├── .bazelrc # Build configuration
├── .bazelversion # Lock Bazel version for team consistency
├── BUILD.bazel # Build instructions for this directory
└── hello.py # Our Python source code
Step 1: Initialize Your Project
Create your project directory:
mkdir bazel-python-tutorial
cd bazel-python-tutorial
Step 2: Create MODULE.bazel - The Modern Way
The MODULE.bazel file is your project's dependency manifest. It's cleaner and more powerful than the old WORKSPACE approach.
# MODULE.bazel
module(
name = "bazel_python_tutorial",
version = "1.0.0",
)
bazel_dep(name = "rules_python", version = "0.29.0")
# Simplified Python setup - let Bazel find system Python
python = use_extension("@rules_python//python/extensions:python.bzl", "python")
python.toolchain(
python_version = "3.11",
)
Key Differences from WORKSPACE:
- No need for
http_archive
or SHA hashes - Automatic version resolution
- Cleaner, more declarative syntax
- Built-in dependency management
Step 3: Create .bazelrc - Build Configuration
# .bazelrc
# Configuration file that sets default behavior for Bazel commands
# Think of this as your "build preferences"
# === Build Configuration ===
build --verbose_failures
build --show_progress_rate_limit=5
test --test_output=errors
test --test_summary=detailed
common --enable_bzlmod
Step 4: Lock Bazel Version (.bazelversion)
7.1.1
Step 5: Create BUILD.bazel - Build Instructions
# BUILD.bazel
"""
Build instructions for the root directory.
Every directory containing source code needs a BUILD.bazel file.
"""
# Import the py_binary rule from Python rules
load("@rules_python//python:defs.bzl", "py_binary")
# Define a Python executable target
py_binary(
name = "hello",
srcs = ["hello.py"], # Source files to include
main = "hello.py", # Entry point file
)
Understanding py_binary:
-
name
: What you'll type inbazel run //:{name}
-
srcs
: List of Python files this target includes -
main
: Which file containsif __name__ == "__main__":
-
python_version
: Ensures Python 3 compatibility
Step 6: Write Your Python Code
#!/usr/bin/env python3
# hello.py
"""
Your first Bazel Python program!
This demonstrates modern Bazel with Python.
"""
import sys
from typing import List
def create_greeting(name: str, enthusiasm_level: int = 1) -> str:
"""
Create a personalized greeting with variable enthusiasm.
Args:
name: The name to greet
enthusiasm_level: Number of exclamation marks (1-3)
Returns:
A formatted greeting string
"""
exclamation = "!" * min(max(enthusiasm_level, 1), 3)
return f"Hello, {name}{exclamation}"
def display_bazel_info() -> None:
"""Display information about this Bazel build."""
print("🚀 Modern Bazel + Python Demo")
print(f"Python version: {sys.version}")
print(f"Running from: {__file__}")
print("Built with: Bazel + MODULE.bazel (Bzlmod)")
print("-" * 50)
def main(args: List[str] = None) -> None:
"""
Main entry point of our application.
Args:
args: Command line arguments (optional)
"""
display_bazel_info()
# Basic greeting
print(create_greeting("Bazel World"))
# Enthusiastic greeting
print(create_greeting("Modern Python Developer", 3))
# Success message
print("\n✅ Congratulations! You've successfully:")
print(" • Set up modern Bazel with MODULE.bazel")
print(" • Built your first py_binary target")
print(" • Used Bzlmod for dependency management")
print(" • Created a reproducible, scalable build")
if __name__ == "__main__":
main(sys.argv[1:])
Building and Running Your First Modern Bazel Python Program
Build the Project
# Build your hello target
bazel build //:hello
What happens behind the scenes:
- Bazel reads MODULE.bazel and downloads Python rules
- Sets up Python 3.11 toolchain
- Analyzes BUILD.bazel to understand dependencies
- Compiles and packages your Python code
- Creates executable in
bazel-bin/
Run the Program
# Run your hello target
bazel run //:hello
Expected Output:
🚀 Modern Bazel + Python Demo
Python version: 3.11.x (main, ...)
Running from: /path/to/your/project/hello.py
Built with: Bazel + MODULE.bazel (Bzlmod)
--------------------------------------------------
Hello, Bazel World!
Hello, Modern Python Developer!!!
✅ Congratulations! You've successfully:
• Set up modern Bazel with MODULE.bazel
• Built your first py_binary target
• Used Bzlmod for dependency management
• Created a reproducible, scalable build
Understanding Bazel Labels
The //:hello
syntax is called a "Bazel label":
-
//
= Root of the workspace (where MODULE.bazel lives) -
:
= Separator between package and target -
hello
= Target name (fromname = "hello"
in BUILD.bazel)
Useful Commands to Try
# List all available targets in your project
bazel query //...
# Get detailed information about the hello target
bazel query //:hello --output=build
# See what files Bazel generated
ls -la bazel-bin/
# Clean all build outputs
bazel clean
# Build with verbose output (great for learning)
bazel build //:hello --verbose_failures --announce_rc
What Makes This Better Than Traditional Python?
Reproducible Builds
Every developer on your team will get identical builds because:
- Python version is locked via MODULE.bazel
- Bazel version is locked via .bazelversion
- All dependencies are precisely specified
Incremental Builds
Change one line in hello.py and rebuild:
# Edit hello.py, then rebuild
bazel build //:hello
Bazel only rebuilds what changed - incredibly fast!
Scalability Foundation
This simple setup scales to:
- Hundreds of Python modules
- Mixed-language projects (Python + C++ + Java)
- Microservices architectures
- Thousands of developers
Hope this helps! Follow me for more such updates!
https://www.linkedin.com/in/sushilbaligar/
https://github.com/sushilbaligar
https://dev.to/sushilbaligar
https://medium.com/@sushilbaligar
Top comments (0)