The Config Blender
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
- Cloudflare
Problem
Configuration arrives in layers: a base config and an override that should win wherever the two collide. Merge them into one dict where any key in override replaces the matching base value, except when both sides hold a nested dict at that key, where the same rule plays out one level deeper, all the way down. Keys present only in base carry through untouched, and neither input may be modified.
Config collision. The surviving values after a merge.
Example 1
Input:
base = {"a":1,"b":{"x":10,"y":20}}, override = {"b":{"y":30,"z":40},"c":3}Output:
{"a":1,"b":{"x":10,"y":30,"z":40},"c":3}Example 2
Input:
base = {"db":{"host":"localhost","port":5432}}, override = {"db":{"host":"prod.db.com"}}Output:
{"db":{"host":"prod.db.com","port":5432}}Worked solution and explanation
What this really is
Strip the config-management costume off and this is a single recursive overwrite with exactly one interesting branch. For every key in the override you make one decision: descend, or replace. You descend ONLY when both sides hold a dict at that key; in every other collision the override value wins outright. Candidates who reach for base.update(override) look finished in five seconds and are wrong the instant a nested dict appears, because update swaps the whole nested dict instead of blending it. The thing that actually separates people is the mixed-type collision: a dict on one side, a scalar on the other. Forget to guard BOTH isinstance checks and you either crash trying to merge an int or silently keep the wrong value.
How to get there
Step 1: Walk the override's keys, not the base's
The override is the authority on what survives, so walk its keys. Base-only keys need no decision; they just ride along in the copy you started from.
Step 2: Guard the recursion with two isinstance checks
Descend ONLY when the key exists in both and both values are dicts. Any other shape (scalar vs scalar, dict vs scalar, scalar vs dict) is a flat replace where the override value wins. Both isinstance checks must pass before you recurse, or the mixed-type case bites.
Step 3: Copy first so you never mutate the input
Start from dict(base) so keys absent from the override survive, and so you never write into the caller's original dict. The recursion produces fresh dicts at every level it touches.
The solution
def deep_merge(base: dict, override: dict) -> dict:
merged = dict(base)
for key, value in override.items():
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
merged[key] = deep_merge(merged[key], value)
else:
merged[key] = value
return mergedOne branch does all the work: both-are-dicts recurses, everything else replaces.
- Naive: base.update(override)
- base={'db': {'host': 'local', 'port': 5432}} updated with {'db': {'host': 'prod'}} gives {'db': {'host': 'prod'}}. The whole nested dict is replaced and
portis gone. - Correct: recursive merge
- The same inputs give {'db': {'host': 'prod', 'port': 5432}}. The nested dicts are blended, so
portsurvives whilehostis overridden.
Common follow-up questions
- What if the nesting depth could be very large? (Tests awareness of Python's recursion limit and iterative alternatives using an explicit stack.)
- How would you handle list values? (Tests design decision: concatenate, replace, or merge by index?)
- What if you need a three-way merge (base, env, local)? (Tests chaining: `deep_merge`(`deep_merge`(base, env), local).)