Python
Nested Dictionary
Dictionary Update
Data Structures
Coding Tips

Update value of a nested dictionary of varying depth

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python's built-in dict.update() only merges at the top level — nested dictionaries are replaced, not merged. To recursively update a nested dictionary of arbitrary depth, you need a custom function that walks both dictionaries and merges at every level. This is essential when merging configuration files, API responses, or any hierarchical data where you want to preserve existing nested values while updating specific keys.

The Problem with dict.update()

python
1default_config = {
2    "database": {
3        "host": "localhost",
4        "port": 5432,
5        "credentials": {
6            "user": "admin",
7            "password": "secret"
8        }
9    },
10    "logging": {
11        "level": "INFO"
12    }
13}
14
15override = {
16    "database": {
17        "credentials": {
18            "password": "new_password"
19        }
20    }
21}
22
23# dict.update replaces the entire "database" key
24default_config.update(override)
25print(default_config)
26# {'database': {'credentials': {'password': 'new_password'}}, 'logging': {'level': 'INFO'}}
27# host, port, and user are GONE

update() replaced the entire "database" dictionary with the override, losing host, port, and user.

Recursive Deep Merge

python
1def deep_update(base, override):
2    """Recursively update base dict with override dict."""
3    for key, value in override.items():
4        if key in base and isinstance(base[key], dict) and isinstance(value, dict):
5            deep_update(base[key], value)
6        else:
7            base[key] = value
8    return base
9
10# Usage
11default_config = {
12    "database": {
13        "host": "localhost",
14        "port": 5432,
15        "credentials": {"user": "admin", "password": "secret"}
16    },
17    "logging": {"level": "INFO"}
18}
19
20override = {
21    "database": {
22        "credentials": {"password": "new_password"}
23    }
24}
25
26result = deep_update(default_config, override)
27print(result)
28# {
29#   'database': {
30#     'host': 'localhost',
31#     'port': 5432,
32#     'credentials': {'user': 'admin', 'password': 'new_password'}
33#   },
34#   'logging': {'level': 'INFO'}
35# }

Only the password key was updated. All other nested values were preserved.

Non-Mutating Version

The function above modifies base in place. To preserve the original:

python
1import copy
2
3def deep_merge(base, override):
4    """Return a new dict with base deeply merged with override."""
5    result = copy.deepcopy(base)
6    for key, value in override.items():
7        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
8            result[key] = deep_merge(result[key], value)
9        else:
10            result[key] = copy.deepcopy(value)
11    return result
12
13merged = deep_merge(default_config, override)
14# default_config is unchanged

Setting a Value at an Arbitrary Path

Update a specific key path like ["database", "credentials", "password"]:

python
1def set_nested(d, keys, value):
2    """Set a value in a nested dict using a list of keys."""
3    for key in keys[:-1]:
4        d = d.setdefault(key, {})
5    d[keys[-1]] = value
6
7config = {}
8set_nested(config, ["database", "credentials", "password"], "secret123")
9print(config)
10# {'database': {'credentials': {'password': 'secret123'}}}

setdefault creates intermediate dictionaries if they do not exist.

Getting a Value at an Arbitrary Path

python
1def get_nested(d, keys, default=None):
2    """Get a value from a nested dict using a list of keys."""
3    for key in keys:
4        if isinstance(d, dict):
5            d = d.get(key, default)
6        else:
7            return default
8    return d
9
10config = {"database": {"credentials": {"password": "secret"}}}
11print(get_nested(config, ["database", "credentials", "password"]))
12# secret
13print(get_nested(config, ["database", "missing", "key"], "fallback"))
14# fallback

Deleting a Nested Key

python
1def delete_nested(d, keys):
2    """Delete a key from a nested dict."""
3    for key in keys[:-1]:
4        d = d.get(key, {})
5        if not isinstance(d, dict):
6            return  # Path doesn't exist
7    d.pop(keys[-1], None)
8
9config = {"database": {"credentials": {"user": "admin", "password": "secret"}}}
10delete_nested(config, ["database", "credentials", "password"])
11print(config)
12# {'database': {'credentials': {'user': 'admin'}}}

Using Libraries

For production code, use established libraries:

python
1# deepmerge library
2from deepmerge import always_merger
3
4result = always_merger.merge(base_dict, override_dict)
5
6# pydantic-settings for config merging
7# merges environment variables, .env files, and defaults automatically
8
9# python-box for dot-access nested dicts
10from box import Box
11
12config = Box({"database": {"host": "localhost"}})
13config.database.host = "production-server"

Merging Multiple Dictionaries

python
1from functools import reduce
2
3def deep_merge_all(*dicts):
4    """Deep merge multiple dictionaries left to right."""
5    return reduce(deep_merge, dicts)
6
7defaults = {"a": {"x": 1, "y": 2}}
8env_config = {"a": {"y": 3, "z": 4}}
9cli_args = {"a": {"z": 5}, "b": 10}
10
11final = deep_merge_all(defaults, env_config, cli_args)
12print(final)
13# {'a': {'x': 1, 'y': 3, 'z': 5}, 'b': 10}

Common Pitfalls

  • Using dict.update() for nested merging: update() replaces nested dictionaries entirely instead of merging them. Always use a recursive function for nested data.
  • Modifying the original dictionary: The in-place deep_update function mutates base. If you need to preserve the original, use copy.deepcopy() first or use the non-mutating deep_merge version.
  • Infinite recursion with circular references: If a dictionary contains references to itself (e.g., d["self"] = d), recursive merge loops forever. Add a depth limit or a visited-set guard for untrusted input.
  • Non-dict values at merge points: If base["key"] is a string but override["key"] is a dict (or vice versa), the recursive check isinstance(base[key], dict) and isinstance(value, dict) correctly replaces instead of merging. Ensure this behavior matches your expectations.
  • List merging: The recursive function above replaces lists entirely. If you need to merge lists (append, deduplicate, or zip), add explicit list handling: if isinstance(base[key], list) and isinstance(value, list): base[key].extend(value).

Summary

  • dict.update() only merges the top level — nested dicts are replaced, not merged
  • Use a recursive deep_update function to merge at every nesting level
  • Use copy.deepcopy() for a non-mutating merge that preserves the original
  • Use setdefault in a loop to set values at arbitrary key paths
  • For production code, consider libraries like deepmerge or python-box
  • Handle edge cases: circular references, type mismatches at merge points, and list values

Course illustration
Course illustration

All Rights Reserved.