The Slow Leak
A medium Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- medium
- Seniority
- L3
- Asked in
- Foursquare
Problem
A metrics rollup arrives in nested: a list where each element is either a leaf reading or another sub-group, with sub-groups nested to arbitrary depth. Return one flat list of every reading in left-to-right order. Treat a string as a single reading that stays intact rather than splitting into characters, and let an empty sub-group contribute nothing.
Nested groups. One flat stream.
Example 1
Input:
nested = [1,[2,3],[4,[5]]]
Output:
[1,2,3,4,5]
Example 2
Input:
nested = ["ok",[],["warn",["crit"]]]
Output:
["ok","warn","crit"]
Worked solution and explanation
What this really is
Strip the metrics costume and this is depth-first flattening of a tree, with one landmine buried in Python's type system: a string is iterable. The instant you decide 'if it is iterable, recurse into it,' you have signed up to recurse into 'crit' character by character, then into 'c', which is a length-1 string that is iterable again, and so on until the stack gives out. Anyone can write the recursion. What separates candidates is the single guard that pulls strings out as leaves, and writing it BEFORE the iterability check, not after.
The trap
- Iterability check first
if hasattr(item, '__iter__'): recurse. A string passes that test, so 'crit' recurses into 'c', 'r', 'i', 't', each a length-1 string that is iterable again. The descent never reaches a non-iterable, so you get a RecursionError or an effective hang.- Leaf check first
if isinstance(item, str) or not hasattr(item, '__iter__'): append. Strings short-circuit into the result before the iterability test ever runs, and only real sub-groups get recursed into.
Building it
Step 1: Classify each element
For each element, decide leaf or group. A leaf is a string or anything without __iter__; everything else is a sub-group. Putting the string clause first is what makes the type guard correct, not just present.
Step 2: Recurse into sub-groups
When an element is a sub-group, flatten it with the same function and splice its readings in with extend. Recursion handles arbitrary depth for free: you never need to know how deep the export nests.
Step 3: Let order fall out of the traversal
Because you append leaves and splice recursive results in the order you meet them, the natural left-to-right, depth-first order falls out with no sorting or bookkeeping. Empty sub-groups simply yield an empty splice.
The solution
def flatten_iter(nested) -> list:
result = []
for item in nested:
if isinstance(item, str) or not hasattr(item, "__iter__"):
result.append(item)
else:
result.extend(flatten_iter(item))
return resultThe string check sits ahead of the iterability check on purpose.
Common follow-up questions
- Rewrite this iteratively so a pathologically deep rollup cannot overflow the call stack. (Tests using an explicit stack to trade recursion depth for heap space.)
- What changes if the caller wants a lazy generator instead of a fully materialized list? (Tests yielding leaves lazily with yield and yield from.)
- What breaks if one of the sub-groups is an infinite stream, and how would you bound it? (Tests recognizing that eager collection over an unbounded source never returns.)