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 with
ingest_record_offset(), or preferingest_records_offset()for bulk data, 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. Do not featureingest_record_nowait()in examples: it spawns a detached task and is not safely synchronized withflush(). - 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
Avro record format support is always compiled in — the avro feature is enabled on the
databricks-zerobus-ingest-sdk dependency in python/rust/Cargo.toml. The avro pip
extra installs fastavro, which is required only for encoding dict records; pre-encoded
Avro bytes need no extra.
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_async_runtimes::tokio::init(). - PyO3 API: The bindings use PyO3 0.29 and the
Bound<'py, T>API. UsePython::attach(notwith_gil),Python::detach(notallow_threads), andcast(notdowncast).Py<T>is not unconditionallyClone— useclone_ref(py)under an attached interpreter. - 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; tested through 3.14 (free-threaded builds are not supported)
- Rust >= 1.88 required for the wrapper crate (Tonic 0.14.6 MSRV)
- Line length: 120 (black)
- Formatter: black, isort