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

Recursive deep merge with override precedence
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 merged

One 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 port is gone.
Correct: recursive merge
The same inputs give {'db': {'host': 'prod', 'port': 5432}}. The nested dicts are blended, so port survives while host is overridden.

Common follow-up questions