Deep Merge Layered Configuration Maps
Merge nested configuration mappings layer by layer, replacing scalars and lists while keeping the default tree untouched.
Code
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()
Report a problem with this exampleOpens a pre-filled issue on GitHub.