The Column Transformer
An easy Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- easy
- Seniority
- L3
- Asked in
- HelloFresh
Problem
An ETL step ships its per-column transforms as configuration: transforms maps a column name to the name of an operation, while rows is a list of same-keyed dicts. An operation is one of 'upper' or 'lower' to change a string's case, 'increment' to add one to a number, or 'double' to multiply a number by two. Apply each column's operation to its value in every row and return the updated rows without modifying the input.
Each column gets its function.
Example 1
Input:
rows = [{"age":30,"name":"alice"},{"age":25,"name":"bob"}], transforms = {"name":"upper"}Output:
[{"age":30,"name":"ALICE"},{"age":25,"name":"BOB"}]Example 2
Input:
rows = [{"age":30,"name":"alice"}], transforms = {"age":"increment","name":"upper"}Output:
[{"age":31,"name":"ALICE"}]Worked solution and explanation
What you're really being asked
Strip the ETL costume and this is a config-driven dispatch problem: each column names an operation, so you look up what to do per column and run it. The real work is not the operations, it is rebuilding every row as a NEW dict. Walk the row's OWN items() so untransformed columns pass through and key order survives for free, then overwrite only the columns that name a transform. Get greedy and mutate the row in place and you silently rewrite the list the caller still holds; build the row off the transforms dict instead and you drop every pass-through column and scramble the order.
Break down the requirements
Step 1: Rebuild each row as a fresh dict
Process rows one at a time and start each with an empty new_row. Walking the current row's items() (not the transforms dict) is what keeps every column and its original order, and it means you never touch the input.
Step 2: Decide per column whether to transform
For each column, ask if col in transforms. If it names no operation, copy the value straight across. This one check is what makes untransformed columns pass through unchanged.
Step 3: Dispatch on the operation name
When a column does name an operation, dispatch on the name with if/elif: 'upper' and 'lower' change a string's case, 'increment' adds one, 'double' multiplies by two. Because you iterate the row's columns, a transform naming a column the row lacks is simply never reached, so a missing column can never raise.
The solution
def apply_transforms(rows: list, transforms: dict) -> list:
result = []
for row in rows:
# Copy by walking the row's own items so order and pass-through columns are free.
new_row = {}
for col, value in row.items():
if col in transforms:
op = transforms[col]
if op == "upper":
new_row[col] = value.upper()
elif op == "lower":
new_row[col] = value.lower()
elif op == "increment":
new_row[col] = value + 1
elif op == "double":
new_row[col] = value * 2
else:
new_row[col] = value
else:
new_row[col] = value
result.append(new_row)
return resultHow would this grow on the job?
Common follow-up questions
- How would you let callers add new operations without editing this function? (Tests dispatch-table design: replace the if/elif chain with a dict mapping operation names to functions so new operations are a one-line registration.)
- What if a transform needs to read several columns of the same row? (Tests API design: an operation like a ratio needs more than one column, so the transform would receive the whole row rather than a single value.)
- How would you apply these transforms lazily over a very large dataset? (Tests generator-based streaming: yield each transformed row instead of collecting the whole list, so 100,000 rows don't all sit in memory.)