A feature-flag service fires one registered callback for every flag; how do you trace the duplicated side effect to list repetition?
answer
- Duplication without an exception means shared state
- A cache can manufacture intermittency
- Compare identity across the buckets
- One id means one list
- Read the construction site, not the dispatch
basics
~20 sReproduce past the evaluation cache, then compare identity rather than value: len({id(b) for b in buckets}) == 1 shows the per-flag buckets are one list built by [[]] * n. Rebuild them per flag with a comprehension.
solid answer
~40 sThe symptom is a duplicated effect with no exception, which points at shared state rather than control flow. It looks intermittent only because the service caches flag evaluations at an 83% hit rate, so the registration and dispatch path runs on the 17% of misses — reproduce it deterministically first. Then ask one question: are the per-flag buckets distinct objects? Equality cannot answer it, since empty lists compare equal whether there are five or one; identity can, via `buckets[0] is buckets[1]` or `len({id(b) for b in buckets})`. Registering a single callback and printing `[len(b) for b in buckets]` as `[1, 1, 1]` is the same tell. The construction site reads `self.buckets = [[]] * flag_count`; fix it with `[[] for _ in range(flag_count)]` and leave an identity assertion behind as a regression test.
code
python · 12 linesclass FlagRegistry:
def __init__(self, flag_count):
self.buckets = [[]] * flag_count # one list, flag_count references
def on(self, flag, callback):
self.buckets[flag].append(callback)
registry = FlagRegistry(3)
registry.on(0, print)
print([len(b) for b in registry.buckets]) # [1, 1, 1]
print(len({id(b) for b in registry.buckets})) # 1go deeper
Recognise the shape: containers created with * and then mutated. Being able to say 'check whether those lists are actually the same object' is already the useful contribution in this conversation.
Explain how you would confirm it: identity over equality, id across the buckets, and a minimal reproduction that registers one callback and prints every bucket's length.
Demonstrate the whole loop — reproduce deterministically past the cache, prove aliasing by identity, fix the construction site rather than the symptom, and leave an identity assertion so the regression cannot return quietly.
Own the systemic angle: 'n independent mutable containers' should come from one reviewed helper, and a class of silent state-sharing bugs deserves a detection story — a lint rule or a startup invariant — not only a one-line fix.
### The symptom A duplicated side effect is a nastier bug than an exception, because nothing fails. A registration that should have attached one callback to one flag attaches it, apparently, to all of them: the audit hook fires per flag, the metric is counted several times, and the downstream consumer sees the effect once per flag rather than once. No traceback, no stack to read, just a count that is too high. The service also caches flag evaluations, and at an 83% cache-hit rate the registration and dispatch path only runs on the 17% of misses. That is what makes the report read as 'intermittent' or 'looks like a race' — the same input duplicates or does not, depending on cache state you were not thinking about. The first move is therefore to reproduce deterministically: drive the path that misses the cache, or construct the registry directly in a scratch script, so the behaviour is reproducible before you theorise about threads. ### The diagnosis Once it reproduces, one question decides everything: are the per-flag buckets distinct objects? Equality will not answer it. Empty lists compare equal whether they are one object or five, so `buckets[0] == buckets[1]` is `True` in both the healthy and the broken registry, and a candidate who stops there concludes the opposite of the truth. Identity answers it immediately: * `buckets[0] is buckets[1]` — `True` means aliasing. * `len({id(b) for b in buckets})` — collapses to `1` when every slot references one list. * Simply printing `[len(b) for b in buckets]` after a *single* registration: `[1, 1, 1]` rather than `[1, 0, 0]` is the tell, and it needs no introspection at all. Then read the construction site. `self.buckets = [[]] * flag_count` builds one list and stores flag_count references to it, because repetition copies references and never the objects behind them. Every `on()` call appends into that one list, and every dispatch iterates it, so each callback runs once per flag. ### The fix Construct per item: `[[] for _ in range(flag_count)]` evaluates the `[]` expression once per iteration and yields independent lists. Where the buckets are keyed rather than indexed, `collections.defaultdict(list)` or `{k: [] for k in keys}` does the same job — and `dict.fromkeys(keys, [])` is the exact same bug in dict clothing, since the single default value is evaluated once and shared by every key. Resist the tempting non-fixes. Deduplicating at dispatch time ('only call each callback once') hides the aliasing and leaves the shared list to produce a different symptom later. Making the callback idempotent is defence in depth, not a fix. Locking is answering a question nobody asked: nothing here is concurrent. ### Making it stay fixed Leave an identity assertion behind. A regression test that asserts `registry.buckets[0] is not registry.buckets[1]`, or that registering one callback leaves exactly one non-empty bucket, fails loudly the day someone 'simplifies' the constructor back to repetition. Better still, route the shape through one helper so there is a single reviewed construction site for 'n independent mutable containers', and keep the rule in review: any `* n` whose element is a mutable literal is a defect until proven otherwise. ### Why this is a senior question The mechanic — repetition shares references — is junior material. What is senior is the loop around it: recognising that a duplicated effect with no exception points at shared state rather than at control flow; refusing to accept an 'intermittent' label when a cache can manufacture intermittency; choosing identity over equality as the discriminating check; fixing the construction rather than the symptom; and leaving behind a cheap invariant so the class of bug cannot return silently. Candidates who have operated a real service tend to reach for the identity check within a minute; candidates who have not tend to reach for concurrency.
- Which dict construction hides the same bug?`dict.fromkeys(keys, [])`. The default value is evaluated once, so every key maps to that one list and appending through any key is visible through all of them. Use `{k: [] for k in keys}` or `collections.defaultdict(list)`, both of which construct a fresh list per key.
- How do you stop this regressing after the fix?Assert identity in a test: registering one callback must leave exactly one non-empty bucket, and `buckets[0] is not buckets[1]` must hold. Then route the shape through a single construction helper so no call site writes the repetition by hand. Identity assertions are cheap and fail loudly when someone simplifies the constructor.
- Why did the cache make the report look like a race?With most evaluations served from cache, the registration path ran only on misses, so the duplicated effect appeared for some requests and not others with no visible pattern. That shape invites a concurrency theory. Bypassing the cache makes it reproduce every time, after which one identity check settles it.
saying these in an interview costs you the question
- Blames a thread race before checking object identity
- Compares the buckets with `==` and concludes they differ
- Adds dispatch-time deduplication instead of fixing construction
- Assumes the evaluation cache caused the duplicate effect
- Says the buckets are distinct because len() looks right
- Rebuilds the buckets with repetition again after the fix