Debug a Tag Validator: Phantom Nulls, Buried Lists, Null vs Empty
StripeSoftware Engineer · Technical ScreenLast updated
Given a small, working deserialization library with three planted bugs, trace each bad output back to its source, fix it in place, and add a regression test. Tests careful root-causing over quick patching, and recognizing which 'empty' representation should be canonical.
You're handed a small Python library that deserializes raw tag submissions (strings, numbers, and sometimes nested lists of the same) into a flat list of normalized tag strings. It is run over a fixed batch of inputs, the outputs are recorded, and the recording is compared against an expected-output file. Three discrepancies are reported:
- Some recorded lists contain
Noneentries; none are expected. - Some recorded lists are nested, like
["a", ["b", ["c", "d"], "e"], "f"], where["a", "b", "c", "d", "e", "f"]is expected. - For inputs that should produce "no tags," some runs record
[]and others recordNone. Every such case must produce the same value.
The library distinguishes "field not supplied at all" from None using its own sentinel, MISSING, which is never equal to None.
Constraints and Clarifications
- The expected-output file is the specification. Do not edit it or post-process the recorded output.
- Order of surviving tags matters and must be preserved.
- A legitimate falsy value (e.g. the tag
0) must still appear in the output — don't drop everything falsy. - Nesting can go arbitrarily deep.
Part 1 — Stray None entries
Find the exact call that lets a None enter a recorded list, and stop it there rather than filtering the output afterward.
Pick one input that produces a stray None and find the first function whose return value already contains it. What should that function return instead, for that input?
Part 2 — Nested lists
Make every result flatten completely and in original order, no matter how deep the input nesting is.
A recursive call that returns a list needs its *contents* merged in, not the list itself tacked on as one entry.
Part 3 — Null versus empty list
Pick one canonical value for "this field has nothing to report," make every code path that can produce "nothing" agree on it, and justify the choice.
What a complete fix should cover
- Each bug traced to its source function, fixed there, with a one-line reason why the fix belongs there
- A regression test per bug, run against the buggy code first to confirm it reproduces the bug, then against the fix
- No loss of legitimate falsy values (
0,""is different from "absent" if it was explicitly submitted — unless your design says an empty-after-trim string carries no information, state that assumption explicitly)
Hints
Hint 1
Reproduce bug 1 first with the smallest possible input: a single invalid tag value. Find the function that silently falls off the end of its logic without an explicit return.
Hint 2
For bug 2, look for a recursive call whose result is passed to .append() instead of .extend() (or manually looped and merged).
Hint 3
For bug 3, search for every `return` in the deserializer and list out what each one returns for 'no items' — you likely have two different answers scattered across two call sites.
Hint 4
Decide your sentinel discipline up front: MISSING means 'don't contribute to the output at all', never None, never [].
Follow-up questions
- How would you fuzz-test this deserializer to find bugs like these automatically, instead of relying on a fixed batch of inputs?
- If `MISSING` needs to be JSON-serializable for a client, what would you use instead, and what breaks if you reuse `null`?
- How would the fix change if an empty-after-trim string should be kept as a legitimate (if odd) tag rather than dropped?
- Add a `max_depth` guard against pathologically deep nesting — where does it belong, and what should happen when it's exceeded?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.