The Lazy Stream
A hard Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- hard
- Seniority
- L5
- Asked in
- Foursquare
Problem
An upstream export arrives as a data list where some slots hold a lone value and others hold a short list of values that got batched together during ingestion. Return a flat list that splices each batched list's contents into its place, leaving everything else exactly as it sits (strings stay whole even though they iterate, and anything nested deeper than one level is left as-is).
Some slots hold one value, some hold a batch. Level them out.
Example 1
Input:
data = [1,[2,3],4,[5,6]]
Output:
[1,2,3,4,5,6]
Example 2
Input:
data = ["hello",["world"]]
Output:
["hello","world"]
Worked solution and explanation
One level, and one trap
This is one-level flattening wearing a generator costume. The real skill: knowing that yield from expands exactly one level, not recursively, and remembering that a str is iterable, so it needs an explicit guard. Anyone can loop and call isinstance. What separates candidates is branching on list specifically (not a generic iterable check) so 'abc' rides through whole, and resisting the urge to recurse into deeper nesting. Get the guard wrong and 'abc' comes back as 'a', 'b', 'c'; recurse too far and [1, [2, [3, 4]], 5] loses its inner [3, 4].
Break down the requirements
Step 1: Flatten exactly one level
If an element is a list, splice each of its items into place. Do not reach into the items themselves. That bright line is what keeps this from becoming a deep flatten: [1, [2, [3, 4]], 5] must come back as [1, 2, [3, 4], 5], with the inner [3, 4] intact.
Step 2: Treat strings as leaves
Even though str is iterable, a string stays intact. Check isinstance(item, list) rather than a generic iterable test, so 'hello' is yielded whole instead of exploding into 'h', 'e', 'l', 'l', 'o'. This is the single mistake that fails the most submissions on this shape.
Step 3: Use a generator and materialize at the boundary
Define an inner generator that does yield from item for lists and yields the item directly otherwise. Materialize with list(...) at the return so the public signature still returns a list, while the generator keeps auxiliary memory at O(1) if a downstream caller ever switches to streaming.
The solution
def lazy_flatten(data: list) -> list:
def _gen(items):
for item in items:
if isinstance(item, list):
yield from item
else:
yield item
return list(_gen(data))Common follow-up questions
- How would you expose the generator directly instead of materializing? (Drop the `list(...)` wrapper and rename to `iter_lazy_flatten`. Discuss when callers benefit (large or infinite inputs) and when materialization is safer.)
- How would you flatten arbitrary depth while still treating strings as leaves? (Recurse on the predicate `isinstance(item, list)` and `yield from` the recursive call. This is exactly the deep flatten variant, still guarding strings.)
- How would you handle generic iterables like tuples and sets? (Replace `isinstance(item, list)` with a check for `Iterable` from `collections.abc` while still excluding `str` and `bytes`. Mention why sets break the in-place ordering guarantee.)