The Fuller Copy
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
- mid
Problem
A scraping pipeline pulls product listings from several sites, and each raw record differs by source: a price may sit under the first entry of offers or as a top-level field, and it arrives as a string like "$1,299.00", while the id and title hide behind different key names. Flatten each raw record into a uniform listing, coercing prices to floats and timestamps to a plain date, and leaving any field that is absent as None. The same listing can be scraped from more than one page under the same id, so when an id repeats, keep the copy with the fewest missing fields.
The same listing lands twice, half-filled each time. Keep what is whole.
Example 1
Input:
records = [{"id":"P100","name":"Aeron Chair","offers":[{"price":"$1,299.00","priceCurrency":"USD"}],"datePosted":"2026-07-01T12:00:00Z","source_site":"chairhub"},{"id":"P200","price":"$499.50","title":"Standing Desk","datePosted":"2026-06-15T08:30:00Z","source_site":"deskmart"},{"sku":"P100","price":1299,"title":"Aeron Chair","source_site":"chairhub"}]Output:
[{"id":"P100","price":1299,"title":"Aeron Chair","currency":"USD","posted_at":"2026-07-01","source_site":"chairhub"},{"id":"P200","price":499.5,"title":"Standing Desk","currency":null,"posted_at":"2026-06-15","source_site":"deskmart"}]Example 2
Input:
records = [{"id":"P300","name":"Desk Lamp","source_site":"lightco"}]Output:
[{"id":"P300","price":null,"title":"Desk Lamp","currency":null,"posted_at":null,"source_site":"lightco"}]Worked solution and explanation
Why this problem exists in real interviews
This is defensive extraction from heterogeneous records with a merge-on-conflict, dressed up as scraper normalization. The skill being probed: can you pull the same logical field out of records where it lives at a different path and under a different key from every source, coerce its type, and then collapse duplicates by choosing the copy that carries the most data? Anyone can write a dict comprehension. The two traps are (1) record['offers'][0]['price'] blows up with KeyError or IndexError the instant a source omits offers, and (2) a dict keyed by id that overwrites on every hit keeps whichever copy was scraped last, so it silently throws away the price and date the other copy had.
Break down the requirements
Step 1: Extract every field through a fallback chain
The id is under id on one site and sku on another; the title is title or name. Write one helper that walks a list of candidate keys and returns the first non-null value, so adding a new source's key name is a one-line change, not a rewrite.
Step 2: Reach into offers defensively, then fall back
The price prefers offers[0].price and falls back to a top-level price. offers is sometimes absent and sometimes an empty list, so guard with isinstance(offers, list) and offers before indexing [0]. Never index a path you have not proven exists.
Step 3: Coerce types, treat missing as None
Prices arrive as "$1,299.00", "1,050", or the int 1299. Strip the currency symbol and thousands separators, then float it; pass ints straight through float(). A timestamp like 2026-07-01T12:00:00Z becomes the date portion before the T. Anything absent stays None rather than crashing.
Step 4: Deduplicate by keeping the fullest copy
Group by the canonical id. When an id repeats, keep the flat record with the most non-null fields. Replace the stored copy only on a strict improvement, which keeps the first-seen copy on a tie and preserves first-appearance ordering for the output.
The solution
def normalize_listings(records):
def first_present(rec, keys):
for k in keys:
val = rec.get(k)
if val is not None:
return val
return None
def parse_price(raw):
if raw is None:
return None
if isinstance(raw, (int, float)):
return float(raw)
cleaned = raw.replace('$', '').replace(',', '').strip()
return float(cleaned) if cleaned else None
def parse_date(raw):
return raw.split('T')[0] if raw else None
def flatten(rec):
offers = rec.get('offers')
offer = offers[0] if isinstance(offers, list) and offers else {}
price_raw = offer.get('price')
if price_raw is None:
price_raw = rec.get('price')
return {
'id': first_present(rec, ['id', 'sku']),
'title': first_present(rec, ['title', 'name']),
'price': parse_price(price_raw),
'currency': offer.get('priceCurrency'),
'posted_at': parse_date(first_present(rec, ['datePosted', 'posted_at'])),
'source_site': rec.get('source_site'),
}
def completeness(flat):
return sum(1 for v in flat.values() if v is not None)
best = {}
order = []
for rec in records:
flat = flatten(rec)
cid = flat['id']
if cid not in best:
best[cid] = flat
order.append(cid)
elif completeness(flat) > completeness(best[cid]):
best[cid] = flat
return [best[cid] for cid in order]- Naive
- out = {} for r in records: out[r['id']] = { 'price': float(r['offers'][0]['price']), ... } Crashes on a missing 'id' key, a source that uses 'sku', an absent 'offers', a price string with a '$', and quietly drops the fuller copy on the second write.
- Correct
- Pull each field through a key-fallback helper, guard the offers index with isinstance, coerce the price and date, and replace a stored id only when the new copy has strictly more non-null fields. Resilient to new key names and missing paths, and it keeps the most complete record.
Common follow-up questions
- A new source site introduces `productName` for the title and nests the date under `metadata.datePosted`. How does your code change? (Tests whether the extraction is data-driven (add a key to a fallback list, add one path helper) rather than hard-coded, which is the resilience the interview follow-up probed.)
- Two copies of the same id have the exact same number of populated fields but different values. Which wins, and how would you make that rule explicit? (Tests tie-breaking policy: strict-improvement keeps first-seen, but a real pipeline might prefer the most recently scraped or a per-field merge.)
- The crawl no longer fits in memory as one list. How do you flatten and dedup a stream of tens of millions of records? (Tests scaling: a streaming reduce keyed by id, or a group-by-id in Spark with a completeness-max reducer, keeping the transform associative.)