Skip to main content
This guide uses kafka-python to acknowledge a consumed message from a Sifr callback. Kafka is a good callback example because broker clients often invoke handlers from worker threads that Sifr did not create.

Why this shape

Crossing into Kafka means more than importing a client. The important contract is what happens when Python calls back into Sifr from another thread. Five rules drive the Kafka path:
  1. Callbacks need an attached policy. @python.callback(...) names the parameter that becomes the Python callable and states lifetime, dispatch, and concurrency.
  2. Foreign threads are explicit. dispatch=foreign means Python-created threads may enter the handler. Captures must be sendable and thread-safe.
  3. Lifetime drains accepted work. lifetime=call keeps the callable only until the declaration returns, then drains it. Longer lifetimes need an opaque owner with deterministic cleanup.
  4. Bridge the broker workflow. Producer/consumer setup, polling, and thread handoff belong in src/python_bridges/. The Sifr declaration stays a typed boundary.
  5. Trust the import root. Authorize kafka under [trust].python. Add [trust].python-native only if the chosen client loads native extensions in-process.
Sifr rejects callback contracts that hide storage, omit shutdown behavior, or capture values that cannot cross the declared thread boundary.

Package setup

Install kafka-python in the root uv project:
sifr.toml

Bridge the poll-and-callback workflow

src/python_bridges/kafka_consumer.py
The bridge owns Kafka client lifetime. The foreign thread is intentional, so the Sifr declaration must use dispatch=foreign.

Declare the callback boundary

Handler failures cross Python as SifrCallbackError and still return through the declared Result channel. If Python also fails, the Python error stays primary.

Retention and cleanup

This example uses call-scoped callbacks. If the consumer must retain a handler across many polls, switch to lifetime=result or lifetime=Self on an opaque owner with close, async_close, context, or async_context cleanup. Owner shutdown must unregister first, reject new entries, drain accepted invocations, and release captures exactly once.

Next steps