DEV Community

Cover image for Modern Bazel with Python- Module 2 - Libraries and Dependencies
Sushil Baligar
Sushil Baligar

Posted on • Edited on

Modern Bazel with Python- Module 2 - Libraries and Dependencies

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
        }
Enter fullscreen mode Exit fullscreen mode

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"],
)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
    ],
)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Build Everything at Once

# Build all targets in the project
bazel build //...

# Query to see all available targets
bazel query //...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)