Why Python Libraries Matter in Bazel
In Module 1, we created a single executable file. But real-world Python projects need:
- Code reuse across multiple programs
- Modular architecture for maintainability
- Dependency management between components
- Visibility controls for code organization
Traditional Python handles this with packages and imports. Bazel takes it further with explicit dependency graphs and incremental builds.
The Problem with Traditional Python Organization
Consider this typical Python project structure:
my_project/
├── utils/
│ ├── __init__.py
│ ├── math_helpers.py
│ └── string_helpers.py
├── main.py
└── requirements.txt
Issues at scale:
- Implicit dependencies: Hard to know what depends on what
- No incremental builds: Change one util, rebuild everything
- Import path confusion: Relative vs absolute imports
- No visibility control: Everything can import everything
How Bazel Solves Library Management
Bazel introduces:
- Explicit dependencies: Every target declares what it needs
- Incremental builds: Only rebuild affected targets
- Clear visibility: Control who can use your code
- Dependency graphs: Visualize and analyze relationships
Let's build this step by step!
Project Structure for Module 2
We'll expand our project to demonstrate library concepts:
bazel-python-tutorial/
├── MODULE.bazel # Same as Module 1
├── .bazelrc # Same as Module 1
├── .bazelversion # Same as Module 1
├── BUILD.bazel # Root build file
├── lib/ # Our library package
│ ├── BUILD.bazel # Library build instructions
│ ├── math_utils.py # Mathematical utilities
│ └── string_utils.py # String processing utilities
└── apps/ # Applications using our libraries
├── BUILD.bazel # Application build instructions
├── calculator.py # Uses math_utils
└── text_processor.py # Uses string_utils
Step 1: Create Library Code
Create the lib/ directory and files
File: lib/math_utils.py
#!/usr/bin/env python3
"""
Mathematical utility functions for Bazel tutorial.
This demonstrates how to create reusable Python libraries in Bazel.
"""
import math
from typing import List, Union
Number = Union[int, float]
def fibonacci(n: int) -> int:
"""
Calculate the nth Fibonacci number using dynamic programming.
Args:
n: Position in Fibonacci sequence (0-indexed)
Returns:
The nth Fibonacci number
Example:
>>> fibonacci(5)
5
>>> fibonacci(10)
55
"""
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
def prime_factors(n: int) -> List[int]:
"""
Find all prime factors of a number.
Args:
n: Number to factorize
Returns:
List of prime factors
Example:
>>> prime_factors(12)
[2, 2, 3]
"""
if n <= 1:
return []
factors = []
d = 2
while d * d <= n:
while n % d == 0:
factors.append(d)
n //= d
d += 1
if n > 1:
factors.append(n)
return factors
def is_perfect_square(n: int) -> bool:
"""
Check if a number is a perfect square.
Args:
n: Number to check
Returns:
True if n is a perfect square, False otherwise
"""
if n < 0:
return False
root = int(math.sqrt(n))
return root * root == n
class Calculator:
"""
Advanced calculator with memory functionality.
Demonstrates class-based library design in Bazel.
"""
def __init__(self):
"""Initialize calculator with zero memory."""
self.memory: Number = 0
self.history: List[str] = []
def add(self, a: Number, b: Number) -> Number:
"""Add two numbers and store result in memory."""
result = a + b
self._update_memory(result, f"{a} + {b} = {result}")
return result
def multiply(self, a: Number, b: Number) -> Number:
"""Multiply two numbers and store result in memory."""
result = a * b
self._update_memory(result, f"{a} × {b} = {result}")
return result
def power(self, base: Number, exponent: Number) -> Number:
"""Calculate base^exponent and store result in memory."""
result = base ** exponent
self._update_memory(result, f"{base}^{exponent} = {result}")
return result
def recall(self) -> Number:
"""Recall the last calculated value."""
return self.memory
def get_history(self) -> List[str]:
"""Get calculation history."""
return self.history.copy()
def clear(self) -> None:
"""Clear memory and history."""
self.memory = 0
self.history.clear()
def _update_memory(self, value: Number, operation: str) -> None:
"""Update memory and add to history."""
self.memory = value
self.history.append(operation)
File: lib/string_utils.py
#!/usr/bin/env python3
"""
String processing utilities for Bazel tutorial.
This demonstrates another library component with different functionality.
"""
import re
from typing import List, Dict, Optional
from collections import Counter
def reverse_words(text: str) -> str:
"""
Reverse the order of words in a string.
Args:
text: Input string
Returns:
String with words in reverse order
Example:
>>> reverse_words("Hello Bazel World")
"World Bazel Hello"
"""
return ' '.join(text.split()[::-1])
def count_vowels(text: str) -> int:
"""
Count vowels in a string (case insensitive).
Args:
text: Input string
Returns:
Number of vowels found
"""
return len(re.findall(r'[aeiouAEIOU]', text))
def camel_to_snake(text: str) -> str:
"""
Convert CamelCase to snake_case.
Args:
text: CamelCase string
Returns:
snake_case string
Example:
>>> camel_to_snake("HelloBazelWorld")
"hello_bazel_world"
"""
# Insert underscore before uppercase letters (except first character)
result = re.sub(r'(?<!^)(?=[A-Z])', '_', text)
return result.lower()
def extract_emails(text: str) -> List[str]:
"""
Extract email addresses from text using regex.
Args:
text: Text containing potential email addresses
Returns:
List of found email addresses
"""
pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
return re.findall(pattern, text)
def find_longest_word(text: str) -> Optional[str]:
"""
Find the longest word in a string.
Args:
text: Input string
Returns:
Longest word, or None if no words found
"""
words = text.split()
return max(words, key=len) if words else None
class TextAnalyzer:
"""
Advanced text analysis functionality.
Demonstrates stateful library classes.
"""
def __init__(self, text: str):
"""
Initialize analyzer with text.
Args:
text: Text to analyze
"""
self.original_text = text
self.words = text.split()
self.clean_words = [
word.lower().strip('.,!?;:"()[]{}')
for word in self.words
]
def word_frequency(self) -> Dict[str, int]:
"""
Calculate word frequency distribution.
Returns:
Dictionary mapping words to their frequencies
"""
return dict(Counter(self.clean_words))
def most_common_words(self, n: int = 5) -> List[tuple]:
"""
Get the most common words.
Args:
n: Number of top words to return
Returns:
List of (word, frequency) tuples
"""
counter = Counter(self.clean_words)
return counter.most_common(n)
def average_word_length(self) -> float:
"""
Calculate average word length.
Returns:
Average length of words in the text
"""
if not self.words:
return 0.0
return sum(len(word) for word in self.words) / len(self.words)
def sentence_count(self) -> int:
"""
Count sentences in the text.
Returns:
Number of sentences (approximate)
"""
sentence_endings = re.findall(r'[.!?]+', self.original_text)
return len(sentence_endings)
def readability_score(self) -> Dict[str, float]:
"""
Calculate basic readability metrics.
Returns:
Dictionary with readability statistics
"""
word_count = len(self.words)
sentence_count = max(self.sentence_count(), 1) # Avoid division by zero
return {
'words_per_sentence': word_count / sentence_count,
'average_word_length': self.average_word_length(),
'unique_word_ratio': len(set(self.clean_words)) / max(word_count, 1)
}
Step 2: Create Library BUILD.bazel
File: lib/BUILD.bazel
"""
Build instructions for utility libraries.
This demonstrates how to create py_library targets in Bazel.
"""
load("@rules_python//python:defs.bzl", "py_library")
# Mathematical utilities library
py_library(
name = "math_utils",
srcs = ["math_utils.py"],
visibility = ["//visibility:public"], # Allow other packages to use this
)
# String processing utilities library
py_library(
name = "string_utils",
srcs = ["string_utils.py"],
visibility = ["//visibility:public"], # Allow other packages to use this
)
# Combined utilities library (depends on both above libraries)
py_library(
name = "all_utils",
deps = [
":math_utils", # Depend on math_utils target in same package
":string_utils", # Depend on string_utils target in same package
],
visibility = ["//visibility:public"],
)
Key Concepts Explained:
-
py_library
: Creates a reusable Python library (not executable) -
visibility
: Controls which other packages can use this library-
["//visibility:public"]
= Anyone can use it -
["//visibility:private"]
= Only this package can use it -
["//apps:__pkg__"]
= Only the //apps package can use it
-
-
deps
: Lists other libraries this target depends on -
:target_name
: References a target in the same package
Step 3: Create Applications Using Libraries
Create apps/ directory
File: apps/calculator.py
#!/usr/bin/env python3
"""
Calculator application demonstrating library usage in Bazel.
This shows how to import and use py_library targets.
"""
# Import from our library - Bazel handles the import path
from lib.math_utils import Calculator, fibonacci, prime_factors, is_perfect_square
def demonstrate_calculator():
"""Demonstrate the Calculator class functionality."""
print("🧮 Calculator Demo")
print("=" * 50)
calc = Calculator()
# Basic operations
result1 = calc.add(15, 25)
print(f"15 + 25 = {result1}")
result2 = calc.multiply(6, 7)
print(f"6 × 7 = {result2}")
result3 = calc.power(2, 10)
print(f"2^10 = {result3}")
print(f"\nLast result in memory: {calc.recall()}")
print("\nCalculation History:")
for operation in calc.get_history():
print(f" {operation}")
def demonstrate_math_functions():
"""Demonstrate standalone math functions."""
print("\n🔢 Math Functions Demo")
print("=" * 50)
# Fibonacci sequence
print("Fibonacci numbers:")
for i in range(8):
fib = fibonacci(i)
print(f" F({i}) = {fib}")
# Prime factorization
numbers_to_factor = [12, 17, 100, 97]
print(f"\nPrime factorization:")
for num in numbers_to_factor:
factors = prime_factors(num)
print(f" {num} = {' × '.join(map(str, factors))}")
# Perfect squares
test_numbers = [16, 17, 25, 30, 36]
print(f"\nPerfect square check:")
for num in test_numbers:
is_perfect = is_perfect_square(num)
status = "✅" if is_perfect else "❌"
print(f" {num}: {status}")
def main():
"""Main application entry point."""
print("🚀 Bazel Python Library Demo - Calculator App")
print("This app uses //lib:math_utils library")
print()
demonstrate_calculator()
demonstrate_math_functions()
print(f"\n✅ Success! This demonstrates:")
print(" • Using py_library targets from other packages")
print(" • Importing library code with clean imports")
print(" • Bazel's dependency management")
if __name__ == "__main__":
main()
File: apps/text_processor.py
#!/usr/bin/env python3
"""
Text processing application demonstrating string utilities.
Shows how to use multiple library functions together.
"""
# Import from our string utilities library
from lib.string_utils import (
TextAnalyzer, reverse_words, count_vowels,
camel_to_snake, extract_emails, find_longest_word
)
def demonstrate_text_functions():
"""Demonstrate standalone text processing functions."""
print("📝 Text Processing Functions Demo")
print("=" * 50)
sample_text = "Hello Bazel World! This is Amazing."
print(f"Original: '{sample_text}'")
print(f"Reversed words: '{reverse_words(sample_text)}'")
print(f"Vowel count: {count_vowels(sample_text)}")
print(f"Longest word: '{find_longest_word(sample_text)}'")
# CamelCase conversion
camel_examples = ["HelloBazelWorld", "PythonIsAwesome", "BuildSystemsRock"]
print(f"\nCamelCase to snake_case:")
for camel in camel_examples:
snake = camel_to_snake(camel)
print(f" {camel} → {snake}")
# Email extraction
email_text = "Contact us at hello@bazel.build or support@example.com for help!"
emails = extract_emails(email_text)
print(f"\nEmail extraction from: '{email_text}'")
print(f"Found emails: {emails}")
def demonstrate_text_analyzer():
"""Demonstrate the TextAnalyzer class."""
print("\n🔍 Text Analyzer Demo")
print("=" * 50)
sample_document = """
Bazel is a fast, scalable, multi-language build system.
Bazel helps developers build software efficiently.
The build system is designed for large codebases and teams.
Bazel supports many programming languages including Python.
"""
analyzer = TextAnalyzer(sample_document.strip())
print("Analyzing document:")
print(f"'{sample_document.strip()[:60]}...'")
print()
# Word frequency analysis
frequency = analyzer.word_frequency()
print("Word frequencies:")
sorted_words = sorted(frequency.items(), key=lambda x: x[1], reverse=True)
for word, count in sorted_words[:8]: # Top 8 words
print(f" '{word}': {count}")
# Most common words
print(f"\nTop 5 most common words:")
for word, count in analyzer.most_common_words(5):
print(f" {word} ({count} times)")
# Readability metrics
metrics = analyzer.readability_score()
print(f"\nReadability metrics:")
print(f" Average words per sentence: {metrics['words_per_sentence']:.1f}")
print(f" Average word length: {metrics['average_word_length']:.1f}")
print(f" Unique word ratio: {metrics['unique_word_ratio']:.1%}")
def main():
"""Main application entry point."""
print("🚀 Bazel Python Library Demo - Text Processor App")
print("This app uses //lib:string_utils library")
print()
demonstrate_text_functions()
demonstrate_text_analyzer()
print(f"\n✅ Success! This demonstrates:")
print(" • Using multiple functions from a py_library")
print(" • Importing specific functions with clean syntax")
print(" • Complex library classes (TextAnalyzer)")
print(" • Bazel's module import system")
if __name__ == "__main__":
main()
Step 4: Create Applications BUILD.bazel
File: apps/BUILD.bazel
"""
Build instructions for applications that use our libraries.
This demonstrates py_binary targets with library dependencies.
"""
load("@rules_python//python:defs.bzl", "py_binary")
# Calculator application - depends on math utilities
py_binary(
name = "calculator",
srcs = ["calculator.py"],
main = "calculator.py",
deps = [
"//lib:math_utils", # Depend on math_utils library from lib package
],
)
# Text processor application - depends on string utilities
py_binary(
name = "text_processor",
srcs = ["text_processor.py"],
main = "text_processor.py",
deps = [
"//lib:string_utils", # Depend on string_utils library from lib package
],
)
# Combined application - depends on all utilities
py_binary(
name = "combined_demo",
srcs = ["combined_demo.py"],
main = "combined_demo.py",
deps = [
"//lib:all_utils", # Depend on combined utilities library
],
)
Understanding Cross-Package Dependencies:
-
//lib:math_utils
: Reference to math_utils target in lib package -
//package:target
: Standard Bazel label format - Dependencies are explicit: You must declare what you use
- Build optimization: Bazel only rebuilds changed dependencies
Step 5: Create Combined Demo
File: apps/combined_demo.py
#!/usr/bin/env python3
"""
Combined demo showing both math and string utilities working together.
Demonstrates how libraries can be composed in applications.
"""
from lib.math_utils import Calculator, fibonacci, prime_factors
from lib.string_utils import TextAnalyzer, reverse_words, camel_to_snake
def math_and_text_demo():
"""Demonstrate using both libraries together."""
print("🔀 Combined Math + Text Demo")
print("=" * 50)
# Generate some math data
calc = Calculator()
fib_numbers = [fibonacci(i) for i in range(8)]
# Convert to text and analyze
fib_text = " ".join(map(str, fib_numbers))
print(f"Fibonacci sequence: {fib_text}")
# Analyze the number sequence as text
analyzer = TextAnalyzer(fib_text)
print(f"Average 'word' length: {analyzer.average_word_length():.1f}")
print(f"Unique numbers: {len(analyzer.word_frequency())}")
# Do some calculations
total = calc.add(sum(fib_numbers), 0)
print(f"Sum of Fibonacci numbers: {total}")
# Convert calculation description to different formats
description = "FibonacciSequenceSum"
snake_case = camel_to_snake(description)
reversed_desc = reverse_words("Fibonacci Sequence Sum")
print(f"\nText transformations:")
print(f" Original: {description}")
print(f" Snake case: {snake_case}")
print(f" Reversed: {reversed_desc}")
def main():
"""Main entry point."""
print("🚀 Combined Library Demo")
print("Using both //lib:math_utils and //lib:string_utils")
print()
math_and_text_demo()
print(f"\n✅ This demonstrates:")
print(" • Using multiple libraries in one application")
print(" • Library composition and integration")
print(" • Clean import statements across packages")
if __name__ == "__main__":
main()
Step 6: Build and Test Everything
Build Individual Libraries
# Build just the math utilities library
bazel build //lib:math_utils
# Build just the string utilities library
bazel build //lib:string_utils
# Build the combined utilities library
bazel build //lib:all_utils
Build and Run Applications
# Build and run calculator app
bazel run //apps:calculator
# Build and run text processor app
bazel run //apps:text_processor
# Build and run combined demo
bazel run //apps:combined_demo
Build Everything at Once
# Build all targets in the project
bazel build //...
# Query to see all available targets
bazel query //...
Understanding Bazel's Dependency Graph
Visualize Dependencies
# See dependency graph for calculator app
bazel query --output=graph //apps:calculator
# See all dependencies for all targets
bazel query 'deps(//...)' --output=graph
Test Incremental Builds
# Build everything once
bazel build //...
# Change one line in lib/math_utils.py, then rebuild
bazel build //...
# Notice: Only affected targets rebuild!
Key Concepts Mastered
1. py_library vs py_binary
-
py_library
: Reusable code, can be imported by other targets -
py_binary
: Executable programs with main() functions
2. Dependency Declaration
- Must explicitly declare all dependencies in
deps = []
- Bazel enforces this - missing deps cause build failures
- Enables precise incremental builds
3. Visibility Control
-
//visibility:public
: Anyone can use this library -
//visibility:private
: Only current package can use it - Custom visibility:
["//apps:__pkg__"]
- only apps package
4. Cross-Package References
-
//package:target
format for referencing other packages - Clear separation between packages
- Explicit dependency management
5. Import Path Handling
- Bazel automatically handles Python import paths
- Import using package structure:
from lib.math_utils import ...
- No need for complex PYTHONPATH manipulation
What's Next?
In Module 3, we'll add testing to our libraries:
-
py_test
targets for unit testing - Test data management
- Test discovery and execution
- Coverage reporting
Ready to continue with Module 3: Testing Framework?
Summary
✅ Created reusable py_library targets
✅ Demonstrated cross-package dependencies
✅ Learned visibility controls
✅ Built multiple applications using shared libraries
✅ Understood Bazel's incremental build system
Your project now has a solid library architecture that scales!
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)