Python wrapper around the Rust core via PyO3 and maturin.
When writing or reviewing client code that uses this SDK, follow the cross-SDK
performance flow from the root CLAUDE.md:
- Idiomatic flow: ingest in a loop (
ingest_record_offset()oringest_record_nowait()), then callflush()once to confirm durability.ingest_record_offset()returns as soon as the record is queued; the SDK sends it and tracks its acknowledgment in the background. - In async code, an
AckCallbackis a good way to track durability without blocking. wait_for_offset()blocks until a specific offset is acknowledged — use it to confirm a specific record before continuing. Acks are ordered, so waiting on the LAST offset confirms all prior records. Avoid calling it after every record in a tight loop, since that limits throughput to one record per round-trip.ingest_record()is deprecated — prefer the offset-based APIs.
python/
├── rust/src/lib.rs # PyO3 bindings (the Rust ↔ Python bridge)
├── zerobus/ # Python package (pure-Python layer over _zerobus_core)
├── tests/ # pytest test suite
└── pyproject.toml # Build config (maturin)
The native extension module is _zerobus_core, exposed as three submodules:
zerobus._zerobus_core.sync— Synchronous APIzerobus._zerobus_core.aio— Async API (Pythonasync/await)zerobus._zerobus_core.arrow— Arrow Flight support
Run from python/:
make dev— Create venv, install deps, build Rust extensionmake build-rust— Release build of the Rust extensionmake develop-rust— Debug build (faster iteration)make test— pytest with coveragemake lint— pycodestyle + autoflakemake fmt— black + autoflake + isort
PyO3 handles memory management automatically via Python reference counting. Key considerations:
- GIL release: Async operations release the GIL, allowing concurrent Python threads. The tokio runtime is initialized at module import via
pyo3_asyncio::tokio::init(). - Object lifetime: PyO3 classes are ref-counted by Python's GC. No explicit free needed, but
close()should still be called to flush pending records before GC cleanup. - Error mapping: Rust
ZerobusErrormaps toZerobusException/NonRetriableExceptionPython exception classes. - Serialization: Proto descriptors and record payloads cross as Python
bytes. JSON records cross as Pythonstr. No extra serialization layer — PyO3 handles the conversion.
The public Python API is defined by the classes and functions in zerobus/. Changes here follow semver:
- Removing a class, method, or parameter is breaking.
- Deprecate with
warnings.warn(..., DeprecationWarning)first. - The PyO3 binding layer (
python/rust/src/lib.rs) is internal; refactoring it is safe as long as the Python-facing API doesn't change.
- PyO3 has minimal overhead for
bytes/strpayloads (no copy for immutable buffers). - Async API avoids GIL contention — prefer it for high-throughput ingestion.
- Batch ingestion (
ingest_records) amortizes the Python→Rust call overhead.
- Every PR must update
python/NEXT_CHANGELOG.mdunder the appropriate section if it changes user-facing behavior. - Update
python/README.mdif the change affects usage, setup, or API surface. - Add or update examples in
python/examples/for new or modified APIs. - Add docstrings for all new public classes/methods. Update type stubs (
_zerobus_core.pyi) if the native module interface changes.
- Version is derived from
python/rust/Cargo.tomlvia maturin (no separate Python version file). - Tag:
python/v<semver>→ triggersrelease-python.yml→ builds wheels for all platforms/Python versions → publishes to PyPI. - On version bump PR: update the version in
python/rust/Cargo.toml, moveNEXT_CHANGELOG.mdcontents toCHANGELOG.md, resetNEXT_CHANGELOG.md.
- Python >= 3.9 required
- Line length: 120 (black)
- Formatter: black, isort