Config-driven dispatch
When you find yourself writing a growing chain of if action == 'backup': ... elif action == 'notify': ..., that's a smell. Every new action means editing the chain, the branches drift out of order, and one big function knows about everything.
The fix is a registry: a plain dict that maps a name to the function that handles it. Instead of branching, you look up the handler and call it.
registry[name] = handler_fn— register a handler under a name.registry[action](payload)— dispatch: find the handler, call it with the data.
This is the plugin pattern. Adding a new action is just registering one more handler — you never touch the dispatch logic. It's the backbone of task runners, event systems, command routers, and webhook processors.
Two things make a registry production-grade: reject duplicate names so you don't silently clobber an existing handler, and fail loudly on an unknown action (a clear KeyError) rather than silently doing nothing.
Syntax
registry = {} # the registry is just a dict
registry[name] = handler_fn # register
handler = registry[action] # look up
result = handler(payload) # dispatch
# vs. the anti-pattern this replaces:
# if action == 'a': ...
# elif action == 'b': ...Worked examples
Register handlers, dispatch by name
def handle_backup(payload):
return {'action': 'backup', 'target': payload.get('target', 'unknown')}
def handle_notify(payload):
return {'action': 'notify', 'channel': payload.get('channel', 'slack')}
registry = {}
registry['backup'] = handle_backup
registry['notify'] = handle_notify
print(registry['backup']({'target': '/home'}))
print(registry['notify']({'channel': 'email'}))
# Output:
# {'action': 'backup', 'target': '/home'}
# {'action': 'notify', 'channel': 'email'}A safe dispatch function (guards both ends)
def register(reg, name, fn):
if name in reg:
raise ValueError(f'Handler already registered: {name}')
reg[name] = fn
def dispatch(reg, action, payload):
if action not in reg:
raise KeyError(f'Unknown action: {action}')
return reg[action](payload)
reg = {}
register(reg, 'cleanup', lambda p: {'deleted': p.get('count', 0)})
print(dispatch(reg, 'cleanup', {'count': 7}))
try:
dispatch(reg, 'launch_rocket', {})
except KeyError as e:
print('rejected:', e)
# Output:
# {'deleted': 7}
# rejected: 'Unknown action: launch_rocket'Adding an action needs no change to dispatch
reg = {}
reg['greet'] = lambda p: f"hi {p['name']}"
reg['shout'] = lambda p: p['text'].upper()
events = [('greet', {'name': 'Sam'}), ('shout', {'text': 'deploy'})]
for action, payload in events:
print(reg[action](payload))
# Output:
# hi Sam
# DEPLOYCommon mistakes & gotchas
registry[action] vs registry[action]()
registry[action] gives you back the function *object* — it doesn't run it. You have to call it: registry[action](payload). Forgetting the call returns the function itself (e.g. <function handle_backup at 0x...>) instead of the result.
Silently overwriting a handler
A bare registry[name] = fn happily replaces an existing handler with the same name — a typo'd duplicate quietly shadows the real one and you debug it for an hour. Guard registration with an if name in registry: raise ValueError(...).
An unknown key crashes with a bare KeyError
registry[action] on a missing key raises KeyError: 'foo' — technically correct but unhelpful. Check if action not in registry first and raise a message that says what went wrong (f'Unknown action: {action}'), or use registry.get(action) and handle None.
Why it matters
Config-driven dispatch is how you build systems that grow without rotting. New event types, commands, and job kinds get added by registering a handler — the core stays small and untouched. Every serious framework, from web routers to message consumers, is a registry at heart.