A webhook endpoint usually receives many event types. payment.captured, payment.refunded, payment.disputed — your handler reacts differently to each. The dispatch is a function-of-event-type.
Two idiomatic shapes:
# Shape A — if/elif chain (3-4 branches max)
def handle(event):
t = event.get("type")
if t == "item.created":
return handle_created(event)
elif t == "item.updated":
return handle_updated(event)
elif t == "item.deleted":
return handle_deleted(event)
else:
return None# Shape B — dispatch table (4+ branches, scales cleanly)
def handle(event):
handlers = {
"item.created": handle_created,
"item.updated": handle_updated,
"item.deleted": handle_deleted,
}
fn = handlers.get(event.get("type"))
if fn:
return fn(event)
return NoneWhy a separate handler per type? Couldn't I just if/elif inside one function?
You can. The split helps when:
For a 3-line response per event type, a single function with if/elif is fine. The split is a tool you reach for when the handler grows.
What about unknown event types?
Always have a default branch. Either ignore-and-log or raise. "Unknown event type" is something you want to see in your logs — a provider might add a new event type and you'd otherwise miss it.
def handle_event(payload):
event = payload.get("event", {})
event_type = event.get("type")
if event_type == "item.created":
return on_created(event)
elif event_type == "item.updated":
return on_updated(event)
elif event_type == "item.deleted":
return on_deleted(event)
else:
print(f"unhandled event type: {event_type}")
return NoneThree consistent steps:
.getif/elif to a dispatch tableAt 4+ branches, the dispatch table is clearer:
HANDLERS = {
"item.created": on_created,
"item.updated": on_updated,
"item.deleted": on_deleted,
"item.archived": on_archived,
"item.restored": on_restored,
}
fn = HANDLERS.get(event_type)
if fn:
fn(event)
else:
print(f"unhandled: {event_type}")Benefits:
elif insertionprint(HANDLERS.keys()) lists supported typesProviders add new event types over time. Your handler should not crash on a type it doesn't recognize. Two strategies:
Never raise on unknown event type — providers will retry, your endpoint will get spammed, real events will queue up behind dead-letter retries.
The dispatch logic is pure: takes a parsed event, returns a result. It's the easiest piece of any webhook handler to test:
assert dispatch({"type": "item.created"}) == "created"
assert dispatch({"type": "item.unknown"}) is NoneUnit tests on dispatch logic are cheap and catch a lot of bugs.
A webhook endpoint usually receives many event types. payment.captured, payment.refunded, payment.disputed — your handler reacts differently to each. The dispatch is a function-of-event-type.
Two idiomatic shapes:
# Shape A — if/elif chain (3-4 branches max)
def handle(event):
t = event.get("type")
if t == "item.created":
return handle_created(event)
elif t == "item.updated":
return handle_updated(event)
elif t == "item.deleted":
return handle_deleted(event)
else:
return None# Shape B — dispatch table (4+ branches, scales cleanly)
def handle(event):
handlers = {
"item.created": handle_created,
"item.updated": handle_updated,
"item.deleted": handle_deleted,
}
fn = handlers.get(event.get("type"))
if fn:
return fn(event)
return NoneWhy a separate handler per type? Couldn't I just if/elif inside one function?
You can. The split helps when:
For a 3-line response per event type, a single function with if/elif is fine. The split is a tool you reach for when the handler grows.
What about unknown event types?
Always have a default branch. Either ignore-and-log or raise. "Unknown event type" is something you want to see in your logs — a provider might add a new event type and you'd otherwise miss it.
def handle_event(payload):
event = payload.get("event", {})
event_type = event.get("type")
if event_type == "item.created":
return on_created(event)
elif event_type == "item.updated":
return on_updated(event)
elif event_type == "item.deleted":
return on_deleted(event)
else:
print(f"unhandled event type: {event_type}")
return NoneThree consistent steps:
.getif/elif to a dispatch tableAt 4+ branches, the dispatch table is clearer:
HANDLERS = {
"item.created": on_created,
"item.updated": on_updated,
"item.deleted": on_deleted,
"item.archived": on_archived,
"item.restored": on_restored,
}
fn = HANDLERS.get(event_type)
if fn:
fn(event)
else:
print(f"unhandled: {event_type}")Benefits:
elif insertionprint(HANDLERS.keys()) lists supported typesProviders add new event types over time. Your handler should not crash on a type it doesn't recognize. Two strategies:
Never raise on unknown event type — providers will retry, your endpoint will get spammed, real events will queue up behind dead-letter retries.
The dispatch logic is pure: takes a parsed event, returns a result. It's the easiest piece of any webhook handler to test:
assert dispatch({"type": "item.created"}) == "created"
assert dispatch({"type": "item.unknown"}) is NoneUnit tests on dispatch logic are cheap and catch a lot of bugs.
Create a free account to get started. Paid plans unlock all tracks.