This guide covers working with Python in Blueprint using Aspect's aspect_rules_py and rules_python.
- Setup
- Project Structure
- Dependencies
- Building and Running
- Testing
- Linting and Formatting
- Advanced Topics
Python projects in Blueprint use:
- Python 3.x (version managed by Bazel)
- pip/uv for dependency resolution
- pyproject.toml for dependency declaration
Blueprint is pre-configured for Python with:
# MODULE.bazel
bazel_dep(name = "rules_python", version = "1.6.3")
bazel_dep(name = "aspect_rules_py", version = "1.6.3")
bazel_dep(name = "rules_uv", version = "0.88.0")Configuration files:
pyproject.toml- Project metadata and dependenciesrequirements/- Lock files for different environmentsgazelle_python.yaml- Gazelle configuration
Typical Python package structure:
my-python-package/
├── BUILD
├── __init__.py
├── module.py
├── __main__.py
├── tests/
│ ├── BUILD
│ ├── __init__.py
│ └── test_module.py
└── README.md
load("@aspect_rules_py//py:defs.bzl", "py_library", "py_binary", "py_test")
py_library(
name = "mylib",
srcs = [
"__init__.py",
"module.py",
],
visibility = ["//visibility:public"],
deps = [
"@pip//requests",
"@pip//pydantic",
],
)
py_binary(
name = "app",
srcs = ["__main__.py"],
main = "__main__.py",
deps = [":mylib"],
)
py_test(
name = "test_module",
srcs = ["tests/test_module.py"],
deps = [
":mylib",
"@pip//pytest",
],
)- Edit
pyproject.toml:
[project]
name = "my_project"
version = "0.1.0"
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0",
]- Update lock files:
./tools/repinThis runs uv pip compile to generate lock files in requirements/.
- Update Gazelle manifest and BUILD files:
# Update manifest with installed packages
bazel run //:gazelle_python_manifest.update
# Generate BUILD files
bazel run //:gazelleReference dependencies in BUILD files:
py_library(
name = "mylib",
srcs = ["mylib.py"],
deps = [
"@pip//requests", # External package
"@pip//requests//:pkg", # Alternative syntax
"//other/package:lib", # Internal dependency
],
)For test-only or dev dependencies:
# Add to requirements/test_requirements.in
pytest>=7.0.0
pytest-cov>=4.0.0
# Update lock files
./tools/repinTo use console scripts from packages:
load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
py_console_script_binary(
name = "black",
pkg = "@pip//black",
)bazel build //path/to:mylib# Run with Bazel
bazel run //path/to:app
# With arguments
bazel run //path/to:app -- --arg1 value1
# With environment variables
bazel run //path/to:app --action_env=DEBUG=1# Binary location
bazel build //path/to:app
ls -l bazel-bin/path/to/app
# Run directly
./bazel-bin/path/to/appUse pytest for testing:
# tests/test_module.py
import pytest
from mypackage import module
def test_function():
result = module.my_function()
assert result == expected_value
def test_with_fixture(tmp_path):
# Test with pytest fixtures
pass
@pytest.mark.parametrize("input,expected", [
(1, 2),
(2, 4),
])
def test_parametrized(input, expected):
assert module.double(input) == expected# Run all Python tests
bazel test //path/to:all
# Run specific test
bazel test //path/to:test_module
# With verbose output
bazel test //path/to:test_module --test_output=all
# With pytest arguments
bazel test //path/to:test_module --test_arg=-v --test_arg=-sInclude test data files:
py_test(
name = "test_with_data",
srcs = ["test_with_data.py"],
data = [
"testdata/input.json",
"testdata/expected.txt",
],
deps = [":mylib"],
)Access in test code:
from rules_python.python.runfiles import runfiles
r = runfiles.Create()
data_path = r.Rlocation("_main/path/to/testdata/input.json")
with open(data_path) as f:
data = f.read()Blueprint uses Ruff for linting and formatting Python code.
Configuration in pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N"]
ignore = ["E501"]# Format all Python files
format
# Format specific file
format path/to/file.py
# Check only (don't modify)
ruff check path/to/file.py# Lint all Python targets
aspect lint //...
# Lint specific target
aspect lint //path/to:target
# Autofix issues
aspect lint --fix //...For local development with IDEs:
# Create venv (for IDE)
python -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -e .Note: Bazel doesn't use this venv; it's only for IDE support.
Add mypy configuration to pyproject.toml:
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = trueAdd to BUILD file:
# Custom rule or aspect for mypy
# (Can integrate with rules_lint)load("@rules_python//python:packaging.bzl", "py_wheel")
py_wheel(
name = "mypackage_wheel",
distribution = "mypackage",
version = "1.0.0",
deps = [":mylib"],
)Build:
bazel build //path/to:mypackage_wheelBlueprint supports building Python container images with pragmas:
# __main__.py
# oci: build
def main():
print("Hello from container!")
if __name__ == "__main__":
main()This automatically generates an image target:
# Build image
bazel build //path/to:image
# Load into Docker
bazel run //path/to:image
# Push to registry
bazel run //path/to:image.pushTo work with notebooks (optional):
- Add jupyter to
pyproject.toml - Run
./tools/repin - Create console script binary:
py_console_script_binary(
name = "jupyter",
pkg = "@pip//jupyter",
)- Run:
bazel run //tools:jupyter -- notebookTo test against multiple Python versions:
py_test(
name = "test_py311",
srcs = ["test.py"],
python_version = "3.11",
deps = [":lib"],
)
py_test(
name = "test_py312",
srcs = ["test.py"],
python_version = "3.12",
deps = [":lib"],
)For Cython modules:
# Custom rule or use rules_python cython support
# (Advanced - refer to rules_python docs)# cli.py
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True)
args = parser.parse_args()
# ...
if __name__ == "__main__":
main()py_binary(
name = "cli",
srcs = ["cli.py"],
main = "cli.py",
deps = [":lib"],
)# app.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World"}py_binary(
name = "app",
srcs = ["app.py"],
main = "app.py",
deps = [
"@pip//fastapi",
"@pip//uvicorn",
],
)Run:
bazel run //path/to:app -- uvicorn app:app --reload# process.py
import pandas as pd
def process_data(input_file, output_file):
df = pd.read_csv(input_file)
# Process...
df.to_csv(output_file, index=False)
if __name__ == "__main__":
process_data("input.csv", "output.csv")py_binary(
name = "process",
srcs = ["process.py"],
data = ["input.csv"],
deps = ["@pip//pandas"],
)# Update manifest
bazel run //:gazelle_python_manifest.update
# Regenerate BUILD files
bazel run //:gazelleCheck that:
- Package has
__init__.py - BUILD file includes the source
- Dependencies are listed in
deps
./tools/repinCheck gazelle_python.yaml configuration:
manifest: gazelle_python.yaml- Use pyproject.toml - Modern Python project configuration
- Pin dependencies - Use lock files for reproducibility
- Type hints - Add type annotations for better code quality
- Test coverage - Aim for high test coverage
- Follow PEP 8 - Use Ruff for consistent style
- Virtual environments - For IDE support only
- Avoid side effects - Keep imports side-effect free
- Explore Testing Guide
- Learn about Docker Images
- Check Troubleshooting