The observer pattern and an event bus
Let a publisher notify many subscribers without knowing who they are.
On this page
Functions as values and dictionaries.
- Let a publisher notify many subscribers without knowing who they are.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
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.
Code example
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']
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
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'] |
Behind the scenes
How Python processes this code
Python runs this source through its interpreter. It creates objects for values and resolves names as each statement runs.
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.