Deep Merge Layered Configuration Maps

Merge nested configuration mappings layer by layer, replacing scalars and lists while keeping the default tree untouched.

PythonConfig Management

Code

snippet
from __future__ import annotations

from collections.abc import Mapping
from copy import deepcopy
from typing import Any


def deep_merge(
    base: Mapping[str, Any],
    override: Mapping[str, Any],
) -> dict[str, Any]:
    merged = deepcopy(dict(base))
    for key, value in override.items():
        current = merged.get(key)
        if isinstance(current, Mapping) and isinstance(value, Mapping):
            merged[key] = deep_merge(current, value)
        else:
            merged[key] = deepcopy(value)
    return merged


def resolve_layers(*layers: Mapping[str, Any]) -> dict[str, Any]:
    result: dict[str, Any] = {}
    for layer in layers:
        result = deep_merge(result, layer)
    return result


def main() -> None:
    defaults = {
        "server": {"host": "127.0.0.1", "port": 8000, "tls": False},
        "features": ["search"],
    }
    file_config = {"server": {"port": 9000}, "features": ["search", "export"]}
    env_config = {"server": {"host": "0.0.0.0"}}

    resolved = resolve_layers(defaults, file_config, env_config)
    print(resolved)
    assert resolved["server"]["port"] == 9000
    assert resolved["server"]["host"] == "0.0.0.0"
    assert resolved["features"] == ["search", "export"]
    assert defaults["server"]["port"] == 8000


if __name__ == "__main__":
    main()