No One Left Behind
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
- L4
Problem
You're reconciling two exported tables given as lists of dicts, left and right, that share a key field: combine them the way a SQL left join does, keeping every left record and merging in the fields of the first right record that shares its key. Every output row must expose the same right-side columns (every field name appearing anywhere in right, except the join key), so an unmatched row, or a match that lacks one of those fields, carries None there.
Every record stays; fill the gaps where a match exists.
Example 1
Input:
key = "id", left = [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}], right = [{"id":1,"score":90}]Output:
[{"id":1,"name":"Alice","score":90},{"id":2,"name":"Bob","score":null}]Example 2
Input:
key = "id", left = [{"id":1,"name":"Ada"},{"id":2,"name":"Ben"},{"id":3,"name":"Cy"}], right = [{"id":1,"score":90},{"id":2,"badge":"vip"}]Output:
[{"id":1,"name":"Ada","badge":null,"score":90},{"id":2,"name":"Ben","badge":"vip","score":null},{"id":3,"name":"Cy","badge":null,"score":null}]Worked solution and explanation
What this really is
Strip the SQL costume and the load-bearing move isn't the match, it's the schema. The naive read is that each output row's right-side columns come from its own matched record: unmatched rows get nothing, matched rows get whatever fields their match happened to carry. That passes the easy cases and produces a ragged result where row 1 has a score, row 2 has a badge, and row 3 has neither. A real left join gives every row the SAME columns: the union of every field name across right. To pull that off you have to collect that union BEFORE you merge a single record, then fill it on every row, from the match where the field is present and None everywhere else. Two right records with different fields is exactly where a copy-the-match solution quietly falls apart. Rescanning right for every left row is the other tell: index it once by key so the join is one linear pass, not O(n*m).
The two-solution split
- Copy-the-match (ragged)
- For each left row, copy only the fields its own match carried. A row that matched a
{score}record getsscore; a row that matched a{badge}record getsbadge; an unmatched row gets neither. Every row can end up with a different column set. - Union schema (uniform)
- Collect the union
{score, badge}from the whole right list first. Every row exposes both columns: filled from the match where present,Nonewhere the match (or the row) has nothing. One fixed schema across the entire output.
Break down the requirements
Step 1: Index the right side and collect its column union
Walk the right list once. Build a dict mapping each record's key value to the record, keeping the FIRST one when a key repeats. In the same pass, collect the union of right-side column names: every field seen across every right record, minus the join key. This union is the schema every output row will carry.
Step 2: Give every row the union schema
For each left record, copy its own fields into a fresh dict, then look up its key in the index. Now walk the union: for each right column, write the match's value if the match exists and has that column, otherwise write None. Because you fill from the union rather than from the match's fields, matched and unmatched rows come out with identical columns.
The solution
def left_join(left, right, key):
right_index = {}
# a dict holds each right-side column once, in first-seen order
right_columns = {}
for record in right:
if record[key] not in right_index:
right_index[record[key]] = record
for column in record:
if column != key:
right_columns[column] = None
result = []
for left_record in left:
joined = dict(left_record)
match = right_index.get(left_record[key])
for column in right_columns:
if match is not None and column in match:
joined[column] = match[column]
else:
joined[column] = None
result.append(joined)
return resultCommon follow-up questions
- How would you handle duplicate keys on the right side? (Tests whether you keep the first match, the last, or fan out into multiple output rows per left record.)
- How would you turn this into an inner join instead? (Tests trimming to only left records that found a right match and dropping the None-fill branch.)
- What if both sides have millions of records and the index no longer fits in memory? (Tests knowledge of sort-merge join as an alternative when the hash index exceeds memory.)