Event-driven Python

Building emitix: a zero-dependency event bus for Python

Why I built a tiny, fully-typed event bus for Python - and how the sync + async, wildcard, and priority pieces fit together.

Most of my projects reach a point where one part of the code needs to tell another part “this just happened” - without the two knowing about each other. That’s the job of an event bus. I wanted one that was tiny, typed, and didn’t drag in dependencies. So I built emitix.

The smallest useful thing

The entire API starts with one decorator and one call:

quickstart.py
import emitix
@emitix.on("user.created")
def send_welcome_email(user):
print(f"Welcome, {user}!")
emitix.emit("user.created", "Ada")

Arguments to emit flow straight through to every handler. No envelope, no schema ceremony - just call it like a function.

Going async

Handlers can be coroutines. Emit them with emit_async:

async.py
@emitix.on("order.placed")
async def charge_card(order):
await payments.charge(order)
await emitix.emit_async("order.placed", order)

Wildcards and priority

Two features that turned out to matter in real code - subscribing to a family of events, and controlling the order handlers run in:

wildcards.py
@emitix.on("order.*") # any order.* event
def audit(event, *args):
log.info("audit", event, args)
@emitix.on("order.placed", priority=10) # runs before lower priorities
def reserve_stock(order):
inventory.reserve(order)

What I left out (on purpose)

  • No broker, no network - it’s in-process by design.
  • No required message classes - strings and args are enough for most apps.

If you want the full feature list - once(), off(), thread-safety - it’s in the project write-up and on PyPI.