skip to content

questions

4

Why does a Python dict dispatch table store `handler` rather than `handler()` as its values?

level: juniorimportance: must knowfreq 60%

answer

  1. Dicts defer nothing
  2. What has already run when the table exists
  3. Values are evaluated once, at build time
  4. Store the function object, call after lookup
  5. `handler` versus `handler()`

basics

~20 s

A dict literal evaluates every value eagerly while the dict is being built, so handler() would run the handler right there and store its return value. Storing the bare name keeps the function object, which you call after the lookup.

solid answer

~40 s

Functions in Python are first-class objects, so `handler` is a value you can put in a dict just like a number. A dict literal is an ordinary expression: every key and every value is evaluated once, at the moment the dict is constructed. Writing `{"start": start()}` therefore calls `start` immediately, before any dispatch happens, and stores whatever it returned — usually `None`. The table then holds results, not handlers, and `table[key]()` fails with a `TypeError` because you are trying to call `None`. The correct form is `{"start": start}`: lookup returns the function object and you invoke it with a separate pair of parentheses, `table[key](args)`. When a handler needs arguments baked in ahead of time, wrap it with `functools.partial` or a lambda so the value stored is still an uncalled callable.

code

python · 16 lines
python
def start():
    print("start ran")
    return "started"

def stop():
    print("stop ran")
    return "stopped"

# Wrong: both handlers run while the dict is being built.
eager = {"start": start(), "stop": stop()}
print(eager)

# Right: the values are the function objects themselves.
handlers = {"start": start, "stop": stop}
print("table built, nothing has run yet")
print(handlers["start"]())

go deeper

for a junior

Recall that a function name without parentheses is a value you can store, and that adding parentheses runs it. Be ready to point at the exact character that makes a dispatch table break and to call the handler after the lookup.

for a middle

Explain the mechanics: a dict display is evaluated eagerly, left to right, when it is constructed, so handler() fires immediately and stores its return value. Show functools.partial for pre-bound arguments and the lambda late-binding trap in a loop.

for a senior

Own where the table lives and who may change it: built once at import below the handlers, frozen with types.MappingProxyType when it is not a registration point, and with one agreed handler signature so branching does not creep back to the call site.

for a principal

Frame the table as a contract, not a micro-optimization. The dispatch key set, the handler signature and whether the mapping is open for third-party registration are API decisions that outlive the code, and the hash lookup is the least interesting property of the whole thing.

## The two things that make a dispatch table work A dict dispatch table rests on two ordinary Python facts, and the `handler` vs `handler()` question is really asking whether you know both. The first is that **functions are first-class objects**. A `def` statement does two things: it builds a function object and it binds that object to a name. The name is not special — you can pass it to another function, put it in a list, return it, or store it as a dict value. `start` is a reference to the object; `start()` is an expression that *invokes* the object and evaluates to whatever it returned. The second is that **a dict display evaluates eagerly**. `{"a": f(), "b": g()}` is not a lazy description of a mapping. It is an expression that is evaluated left to right, right now: `f()` runs, `g()` runs, and the resulting values are stored. Python has no implicit thunking; nothing about being a dict value defers anything. So the buggy table calls every handler at construction time — which is exactly the moment you were trying to avoid calling them — and stores their return values. ## What the bug looks like in practice ```python handlers = {"start": start(), "stop": stop()} # both already ran handlers["start"]() # TypeError: 'NoneType' object is not callable ``` Because most handler functions return `None`, the usual symptom is `TypeError: 'NoneType' object is not callable` at the first dispatch, plus side effects that fired far too early. If the handlers happen to return something callable the failure is subtler still. The fix is to remove the parentheses at build time and add them at call time: ```python handlers = {"start": start, "stop": stop} handlers["start"]() ``` ## Handlers that need arguments The uncalled-callable rule is why `functools.partial` shows up so often next to dispatch tables. If several keys should run the same function with different pre-bound arguments, `partial(write, "warehouse.orders")` produces a new callable with that argument already applied, without invoking `write`. A `lambda` does the same thing, with one classic trap: a lambda closes over the *name*, not the value, so building handlers in a `for` loop with `lambda: work(name)` makes every entry see the loop variable's final value. Bind it explicitly (`lambda name=name: work(name)`) or use `partial`, which captures the value at creation. ## Where and when the table is built Since the values are just references, constructing the dict is cheap — a few dict inserts — but it is not free, and it is not something to redo on every call. A table built inside the dispatch function is rebuilt on each invocation; a table built at module level is built once at import. Module level is the normal choice, with one ordering constraint: every name you reference must already be bound when the module body reaches that line, so the table goes *below* the handler definitions. If a handler must live above the table for readability, or the set of handlers is discovered dynamically, build the dict in a small factory function and call it once. A module-level dict is also mutable global state: any importer can add or replace an entry. That is sometimes the point — it is how plugin registration is often done — but when it is not, wrapping the table in `types.MappingProxyType` gives a read-only view that raises on mutation. ## Signature discipline The table is only useful if every value can be called the same way, because the call site knows the key, not which function it found. Decide on one signature — all handlers take the record, or all take no arguments and read from a closure — and use `partial` to reshape any handler that does not fit. Handlers whose signatures diverge push the branching back into the call site, which defeats the table. ## What the table does and does not buy you The lookup itself is a hash lookup, so it does not scan the entries the way a chain of comparisons would. That is a real property, but it is rarely the reason to write one — with a handful of keys the difference is noise. The reason is structural: the mapping from key to behaviour becomes data you can inspect, iterate, extend and test, and the handlers become independently testable functions. And the lookup being fast says nothing about the handlers: dispatch cost is a hash and a call, and everything expensive is inside the function you just found.

  • How do you add a handler that needs arguments bound ahead of time without calling it when the table is built?
    Wrap it: `functools.partial(write, "warehouse.orders")` returns a new callable with that argument applied and does not invoke `write`. A lambda works too, but a lambda closes over names rather than values, so one built in a loop will see the loop variable's final value unless you bind it with a default argument. `partial` captures the value at creation, which is why it is the safer default inside loops and comprehensions.
  • Should the dispatch dict be built at module level or inside the function that uses it?
    Module level, normally: it is constructed once at import instead of on every call, and it reads as a declaration of the mapping. The constraint is name binding order — the handlers must be defined above it. The cost is that a module-level dict is mutable global state any importer can edit; if that is not wanted, expose a read-only view with `types.MappingProxyType`. Build it inside a factory function only when the entries are discovered dynamically.
  • What has to be true of the handlers' signatures for a dispatch table to actually remove branching?
    They must all be callable the same way, because the call site knows only the key. Pick one shape — every handler takes the record, or every handler takes nothing — and reshape the outliers with `functools.partial` or a small adapter function. If handlers take different arguments, the caller has to know which one it looked up, so the conditional logic reappears at the call site and the table has bought nothing.

A dispatch table is a menu, not a kitchen ticket: it lists dishes you can order, it does not cook them all when the menu is printed.

saying these in an interview costs you the question

  • Claims a dict defers evaluating its values until lookup
  • Writes `{"a": handler()}` and expects the call at dispatch time
  • Rebuilds the dispatch dict inside the function on every call
  • Thinks O(1) lookup means the handlers themselves are cheap
  • Builds handlers with a lambda in a loop and expects per-iteration capture
  • Lets each handler take a different signature and branches at the call site

context

open as a page

How do you handle an unknown key in a Python dict dispatch table without masking errors raised by the handler?

level: middleimportance: should knowfreq 46%

basics

~20 s

Separate the lookup from the call: handlers.get(key, fallback) or a get returning None followed by an explicit raise, then invoke the handler on its own line. Wrapping handlers[key]() in try/except KeyError also catches a KeyError raised inside the handler.

open as a page

When should a dict dispatch table in an ETL export give way to objects with methods instead?

level: seniorimportance: should knowfreq 40%

basics

~20 s

When each branch stops being one stateless verb. Once a record kind needs setup, staged state, a commit and a rollback that belong together, a dict of functions has nowhere to hold the pairing; a table of classes whose instances carry that lifecycle does.

open as a page

What does functools.singledispatch give you that a dict keyed on type(obj) does not?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

functools.singledispatch resolves through the argument's method resolution order and registered abstract base classes, so subclasses and virtual subclasses find a handler. A dict keyed on type(obj) matches the exact class only and misses every subclass.

open as a page