The observer pattern and an event bus
Let a publisher notify many subscribers without knowing who they are.
class EventBus:
def __init__(self):
self.handlers = {}
def subscribe(self, event, handler):
self.handlers.setdefault(event, []).append(handler)
def publish(self, event, payload):
for handler in self.handlers.get(event, []):
handler(payload)
log = []
bus = EventBus()
bus.subscribe("order_placed", lambda order: log.append(f"email {order}"))
bus.subscribe("order_placed", lambda order: log.append(f"stock {order}"))
bus.publish("order_placed", "#42")
bus.publish("order_cancelled", "#43")
print(log)['email #42', 'stock #42']
Explanation
A design pattern is a named, reusable solution to a recurring design problem. The observer pattern lets one object announce that something happened while any number of other objects react, without the announcer knowing them. This decouples the code that detects an event from the code that responds to it.
The example is a small event bus. Code registers a handler for an event name with subscribe. When publish is called with that event name, every registered handler is called with the payload. Placing an order can then trigger an email and a stock update without the order code importing either. Adding a third reaction means adding one subscription, not editing the publisher.
The costs are indirection and implicit control flow: it can be hard to see everything that happens after an event. Handlers run in subscription order here, but code should not depend on ordering unless it is part of the contract. A handler that raises an exception can prevent later handlers from running, so production event systems decide how to isolate failures. Unsubscribe when a subscriber is no longer needed to avoid memory leaks.
Explore the dry run
class EventBus: def __init__(self): self.handlers = {} def subscribe(self, event, handler): self.handlers.setdefault(event, []).append(handler) def publish(self, event, payload): for handler in self.handlers.get(event, []): handler(payload) log = []bus = EventBus()bus.subscribe("order_placed", lambda order: log.append(f"email {order}"))bus.subscribe("order_placed", lambda order: log.append(f"stock {order}"))bus.publish("order_placed", "#42")bus.publish("order_cancelled", "#43")print(log)Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Each event name keeps a list of handlers. subscribe adds one without touching the publisher. | — | — | |
| The first subscriber reacts to placed orders by recording an email. | — | — | |
| Publishing calls every handler registered for order_placed with the payload #42. | — | — | |
| No one subscribed to this event, so nothing happens and no error occurs. | — | — | |
| Both handlers ran, in the order they subscribed. | — | ['email #42', 'stock #42'] |
Cost and common mistakes
publish is O(h) for h handlers of that event. subscribe is O(1) amortized.
- Forgetting to unsubscribe keeps objects alive and can leak memory in long-running programs.
- Relying on handler order, or on one handler's side effects for another, makes event code fragile.
- Too many events make control flow hard to follow. Use direct calls when only one component needs to respond.
Try it yourself
How would you add an analytics reaction to order_placed?
Reveal answer and reasoning
Call bus.subscribe("order_placed", record_analytics). The publisher and the existing handlers stay unchanged.