skip to content

A webhook receiver dedupes with a dict subclass whose __missing__ registers the event id; after a membership guard was added, handlers fire twice. How do you diagnose it?

level: seniorimportance: should knowfreq 20%

answer

  1. Behaviour changed with the access style
  2. Only one syntax reaches the hook
  3. Membership reads the table directly
  4. State change hidden on a read path
  5. Name the operation instead of hooking a miss

basics

~20 s

The membership operator never calls missing, so the guard checks without registering anything and every redelivery looks new. Reproduce it with one assertion, then move registration out of the miss hook into an explicit method the guard calls.

solid answer

~50 s

Start from the mechanism: `__missing__` is consulted only by `dict.__getitem__`, so `if event_id not in seen:` tests the hash table and never runs the registration code. Whatever the hook was doing on a miss — storing the id, starting a timer — now happens nowhere, and each redelivery is treated as first delivery, producing the duplicated side effect. Confirm it in seconds: assert that a membership test leaves `len(seen)` unchanged while a subscript lookup increases it. Then fix the design rather than the guard. A miss hook that mutates state or performs work is a side effect hidden on a path every reader parses as a plain lookup; registration belongs in a named method — `claim(event_id) -> bool` — that the guard calls explicitly. Cover it with a regression case, and audit the codebase for other accessors (`get`, `pop`, `setdefault`, iteration) that were silently skipping the hook all along.

code

python · 10 lines
python
class Seen(dict):
    def __missing__(self, key):
        self[key] = 0        # side effect: registers the id
        return 0

seen = Seen()
if "evt-1" not in seen:      # membership never calls __missing__
    pass
print(dict(seen))            # {} - the guard registered nothing
print(seen["evt-1"], dict(seen))

go deeper

for a junior

The takeaway to carry: the miss hook fires only for square-bracket lookup, so a membership test on the same mapping behaves differently. Be able to demonstrate that difference in a few lines.

for a middle

Explain the diagnosis path — instrument the hook, show it never fires under the new access — and name the other accessors that skip it, so you can predict where else the code has the same hole.

for a senior

Show the production instincts: confirm the mechanism before believing it, rule out per-process state and at-least-once delivery as competing causes, and replace the hidden effect with a named operation covered by a behaviour-level regression case.

for a principal

Own the standard: effects and state changes carry names, containers stay honest on read paths, and idempotency for an at-least-once source belongs in a deliberate, shared, testable mechanism rather than in a subclass hook nobody reviews.

## Reading the failure The symptom is a duplicated side effect: a webhook redelivery runs the handler again. The proximate cause is a change in *how the mapping was accessed*, not in what the mapping stores. That shape — behaviour changed by a read that looks equivalent — is the fingerprint of logic living inside a miss hook. `__missing__` runs on exactly one path: `dict.__getitem__`, which is the `d[key]` syntax. The containment operator, `dict.get()`, `dict.pop()`, `dict.setdefault()`, iteration and the view methods all consult the hash table directly. So the original code, whatever it looked like, was reaching registration through subscription — probably something like `if seen[event_id]: return` — and the refactor to a membership guard removed the only call site that triggered it. ## Confirming it fast Do not reason about it in review; make the interpreter answer. Three lines: ```python seen = Seen() before = len(seen) "evt-1" in seen assert len(seen) == before # passes: membership registered nothing seen["evt-1"] assert len(seen) == before + 1 # the hook only runs on subscription ``` If the mapping is behind an abstraction, instrument the hook itself — a counter incremented in `__missing__`, printed at the end of a request — and observe that it stays at zero under the new code path. In a live service, the same evidence comes from a metric on hook invocations plotted against redeliveries: the two used to move together and now do not. Check the deployment story too. A duplicated side effect at a webhook receiver has several classic causes — retries with an at-least-once sender, a load balancer replaying a request, two instances consuming the same delivery, dedupe state living in a per-process dictionary that a second worker does not share. Confirm the mechanism before you commit to the mapping explanation: if the hook never fires under *any* code path, it is the mapping; if it fires once per process but you have four processes, the dictionary was never the right place for the state. ## The fix, and why the obvious one is wrong The obvious fix is to change the guard back to subscription. Do not. It restores the behaviour by re-establishing an invisible coupling: correctness now depends on nobody ever writing `in`, `.get()`, or a debugger watch expression against that mapping. The next person to make the same reasonable edit reintroduces the same outage. The durable fix is to stop hiding an effect behind a read. Give the container an explicit operation whose name says what it does: ```python class Seen: def __init__(self): self._ids = {} def claim(self, event_id): """Return True if this id had not been seen before.""" if event_id in self._ids: return False self._ids[event_id] = True return True ``` No inheritance from `dict`, no hook, no mapping contract to honour — and the call site reads `if seen.claim(event_id):`, which cannot be refactored into something that silently skips the effect. Note the second benefit: because this is a plain class, the check-and-store pair is now a single method you can make atomic under a lock or back with a shared store, which a bare mapping never let you express. If you must keep a mapping, keep the hook **pure**: let `__missing__` compute and return a default, and store nothing. A pure hook cannot be the difference between one side effect and two. ## Preventing the class of bug Write the regression test at the level the bug lives — a redelivery of an already-processed id must not invoke the handler — rather than asserting on the mapping's internals; the internals are what you want free to change. When the receiver already has a large regression pack (a 340-case corpus of recorded deliveries, say), the useful addition is a small number of *duplicate* cases interleaved into it, not 340 more; the pack's job is to exercise the guard on every handler, and duplicates only need to prove the guard runs at all. Then sweep. Any class that defines `__missing__` with a body doing more than computing a value is a candidate for the same failure. So is any `dict` subclass overriding `__setitem__`, for the mirror-image reason: `update()`, the constructor and `setdefault()` bypass it. Both are cases of the same rule — in CPython, the inherited built-in methods and the dunder hooks agree with each other far less than the code reads as though they do. ## The judgement being tested A senior answer diagnoses in minutes and then argues about design: state changes belong in operations named after the change, containers should not surprise readers on read paths, and a guard whose correctness depends on which accessor a colleague picks is not a guard. Getting to "membership does not call the hook" is table stakes; the interview turns on what you do next.

  • Why not just change the guard back to subscription and ship?
    It restores behaviour by re-establishing an invisible coupling: correctness then depends on nobody ever writing `in`, `.get()` or a debugger watch against that mapping. The same reasonable refactor reintroduces the outage. Fix the design instead — a named `claim()`-style operation makes the state change explicit and impossible to skip accidentally.
  • What else would you check before blaming the mapping for the duplicated side effect?
    That the receiver is not simply losing its dedupe state: an in-process dictionary is per-worker and dies on restart, so redeliveries hitting a second instance or a restarted one duplicate regardless of any hook. Confirm the mechanism — instrument the hook and show it never fires — before you commit to the language explanation, then decide whether the state needs a shared store at all.
  • What is the general rule this bug illustrates?
    That CPython's mapping accessors do not all route through the same code. `__missing__` fires only for subscription, and inherited `dict` methods like `update()` and `setdefault()` bypass an overridden `__setitem__`. Any behaviour you attach to one accessor is absent from the others, so never let a state change or an effect depend on which accessor a caller chose.
  • How would you make the check-and-register step safe under concurrency?
    Not with a bare mapping: `key in d` followed by a write is two operations with a gap between them. Collapsing them into one method — the `claim()` shape — gives you somewhere to hold a lock, or to issue a single conditional write against a shared store. That is another argument for a named operation over a container hook: a hook on a read cannot express atomicity.

saying these in an interview costs you the question

  • Assumes the membership operator triggers the miss hook
  • Fixes it by reverting the guard and stops there
  • Puts state mutation on a path that reads as a lookup
  • Never checks whether dedupe state is per-process
  • Asserts on container internals instead of handler behaviour
  • Adds hundreds of regression cases rather than the missing one

context