# The Event Broadcaster

> Subscribers show up, listen, and sometimes leave.

Canonical URL: <https://datadriven.io/problems/the_event_broadcaster>

Domain: Python · Difficulty: medium · Seniority: L4

## Problem

Implement an EventEmitter that supports on(event, listener), emit(event, payload), and off(event, listener). Listeners are opaque identifiers (strings in the tests). emit notifies every listener currently subscribed to the event by appending the payload to the result list once per listener (so a 3-listener emit returns [payload, payload, payload]); an emit with no listeners returns []. off removes one matching listener identifier (the first one found if duplicates exist). Then implement a driver event_broadcaster(op_names, op_args) that constructs an EventEmitter when op_names[0] is 'EventEmitter' and dispatches the remaining 'on'/'emit'/'off' ops with their parallel arg lists. Return the list of results in op order: None for the constructor, on, and off; the per-listener payload list for emit.

## Worked solution and explanation

### Why this problem exists in real interviews

Two skills in one prompt. The class is the textbook pub/sub pattern (dict of event to list of listeners). The driver is an op-replay loop, the same shape as LRU cache or stack problems where the test harness scripts a sequence of method calls. The interviewer is checking that you can keep them cleanly separated.

---

### Break down the requirements

#### Step 1: Pub/sub registration with first-match removal

Inside `EventEmitter`, keep `self.listeners` as a `dict[str, list]`. `on(event, listener)` appends `listener` to `self.listeners.setdefault(event, [])`. `off(event, listener)` removes the **first** matching listener from that list, ignoring the call if the list is empty or the listener isn't found.

#### Step 2: emit returns one payload per active listener

`emit(event, payload)` returns `[payload] * len(self.listeners.get(event, []))`. The output length is the number of listeners on that event at the moment of emission. Listeners are opaque, so we don't actually call anything; the prompt's contract is to deliver the payload once per listener.

#### Step 3: Driver loop over the parallel op arrays

`event_broadcaster(op_names, op_args)` walks the two parallel lists in lockstep. Index 0 is always `'EventEmitter'` and constructs the instance (result is `None`). For each later op, dispatch on the name and append the result. `on` and `off` produce `None`; `emit` produces a list.

---

### The solution

**EventEmitter plus a driver that replays op_names against op_args**

```python
class EventEmitter:
    def __init__(self) -> None:
        self.listeners: dict[str, list] = {}

    def on(self, event, listener):
        self.listeners.setdefault(event, []).append(listener)

    def emit(self, event, payload):
        return [payload] * len(self.listeners.get(event, []))

    def off(self, event, listener):
        bucket = self.listeners.get(event)
        if not bucket:
            return
        try:
            bucket.remove(listener)
        except ValueError:
            pass


def event_broadcaster(op_names: list[str], op_args: list[list]) -> list:
    emitter = None
    out = []
    for name, args in zip(op_names, op_args):
        if name == 'EventEmitter':
            emitter = EventEmitter()
            out.append(None)
        elif name == 'on':
            emitter.on(*args)
            out.append(None)
        elif name == 'emit':
            out.append(emitter.emit(*args))
        elif name == 'off':
            emitter.off(*args)
            out.append(None)
    return out
```

> **Time and Space Complexity**
>
> **Time:** `on` is O(1). `emit` is O(k) where k is the listener count for that event. `off` is O(k) for the linear scan inside `list.remove`.
> 
> **Space:** O(total registered listeners).

> **Interviewers Watch For**
>
> Strong candidates use `setdefault` and `list.remove` rather than rebuilding the listener list each time. They also notice that the driver is bookkeeping only: keep the EventEmitter logic inside the class so you could lift it into another harness later.

> **Common Pitfall**
>
> Returning `None` from emit. The harness compares the result list element-wise; emit must return a list (possibly empty), not None, or every later assertion shifts.

---

## Common follow-up questions

- How would you support `once(event, listener)` so the listener fires exactly once before being unsubscribed? _(Tests adding an auto-removal flag and a dedicated wrapper.)_
- If listeners were callables instead of opaque identifiers, what would change in `emit`? _(Tests calling user-supplied callables with the payload and collecting return values.)_
- How would you make `EventEmitter` safe when a listener calls `off` on itself during `emit`? _(Tests locking or copy-on-emit to avoid mid-iteration mutations.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/the_event_broadcaster)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). 100% free data engineering interview prep. Live code execution against Postgres 16, Python 3.11, and Spark sandboxes. No paywall, no premium tier, no signup gate.