# Every Line on the Receipt

> Nested deep inside the receipt. Pull every item out.

Canonical URL: <https://datadriven.io/problems/every_line_on_the_receipt>

Domain: Python · Difficulty: medium · Seniority: L5

## Problem

Transaction events arrive as nested structures: each event is a dict with a store_id (str), a timestamp (str), and an items list of dicts (each item has item_name and item_price). Write `flatten_transactions(events)` that explodes every event into one record per item, returning a flat list of dicts where each record carries store_id, item_name, timestamp, and item_price from its parent transaction. An event with an empty items list produces zero records, and an empty input list returns an empty list. Keep events in input order and items in their listed order within each event.

## Worked solution and explanation

### Why this problem exists in real interviews

This is a parent-child explode wearing a receipt costume. Retail point-of-sale data almost never arrives in the shape analytics needs: a transaction is one header (store, timestamp) wrapping a list of line items, and the row count you owe the warehouse is the total number of items, not the number of transactions. The real skill being probed is whether you can carry the header fields DOWN onto every child while nested-iterating, and whether you trust an empty inner list to disappear on its own. Anyone can write the two loops. The tell is what you do with a voided transaction: reach for an `if not items` branch and you corrupt the fact table.

---

### Break down the requirements

#### Step 1: Walk the events in input order

The outer structure is a list of transaction dicts. Iterate it as given so the emitted rows stay deterministic; nothing here should sort or regroup.

#### Step 2: Explode each event's items

For each event, iterate its items list in its listed order. Each item becomes exactly one output record, so the inner loop is where rows are actually born.

#### Step 3: Carry the header onto every line

Read store_id and timestamp once per event, then stamp them onto each item's record alongside item_name and item_price. Every output dict has exactly these four keys.

#### Step 4: Let empty lists drop out by construction

An event with an empty items list runs the inner loop zero times and contributes nothing; an empty events list yields an empty result. No special-case branch, and never a placeholder or None-filled row.

---

### The solution

**flatten_transactions**

```python
def flatten_transactions(events):
    rows = []
    for event in events:
        store_id = event['store_id']
        timestamp = event['timestamp']
        for item in event['items']:
            rows.append({
                'store_id': store_id,
                'item_name': item['item_name'],
                'timestamp': timestamp,
                'item_price': item['item_price'],
            })
    return rows
```

*Outer loop pins the header, inner loop emits one row per item.*

> **Cost**
>
> O(N) where N is the total number of line items across all events: each event header is read once, each item visited once. Space is O(N) for the flat output list plus O(1) auxiliary working memory. Hoisting store_id and timestamp out of the inner loop avoids re-reading the header for every item.

> **What signals seniority**
>
> The nested loop IS the explode: the outer loop pins the parent fields, the inner loop emits one record per item. A candidate who names this as UNNEST/LATERAL in SQL or explode() in Spark shows they know the shape. A double-for comprehension (for event in events for item in event['items']) is the idiomatic one-expression equivalent; the explicit loops win here only because hoisting the header reads cleaner.

> **The branch that corrupts the table**
>
> The classic mistake is special-casing empty items with an `if not event['items']` branch that appends a placeholder row full of None values. That inflates item counts and poisons every downstream aggregation. Let the inner loop's natural emptiness do the work: zero items means zero rows, no branch needed.

---

### Follow-up questions an interviewer may ask

## Common follow-up questions

- How would this change if events streamed in one at a time and could not all fit in memory? _(Tests whether they reach for a generator that yields rows lazily instead of building the full list.)_
- What if some events were missing the store_id or items key entirely? _(Probes defensive access with .get and defaults, and the decision to skip versus raise.)_
- How would you parallelize this across a large batch, and where would input order be preserved or lost? _(Checks understanding of partitioning and where ordering guarantees survive a distributed explode.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/every_line_on_the_receipt)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). DataDriven is the data engineering interview community. Live code execution in SQL, Python, and Spark sandboxes. Every feature is open to every member.