'Python: How to recursively merge 2 dictionaries? [duplicate]

Suppose we have 2 dictionaries:

a = {
    "key1": "value1",
    "key2": "value2",
    "key3": {
        "key3_1": "value3_1",
        "key3_2": "value3_2"
    }
}

b = {
    "key1": "not_key1",
    "key4": "something new",
    "key3": {
        "key3_1": "Definitely not value3_1",
        "key": "new key without index?"
    }
}

As a result of the merger, I need to get the following dictionary:

{
    "key1": "not_key1",
    "key2": "value2",
    "key3": {
        "key3_1": "Definitely not value3_1",
        "key3_2": "value3_2",
        "key": "new key without index?"
    },
    "key4": "something new"
}

I have this kind of code:

def merge_2_dicts(dict1, dict2):
    for i in dict2:
        if not type(dict2[i]) == dict:
            dict1[i] = dict2[i]
        else:
            print(dict1[i], dict2[i], sep="\n")
            dict1[i] = merge_2_dicts(dict1[i], dict2[i])
    return dict1

It works and gives me the desired result, but I'm not sure if it can be done more simply. Is there an easier/shorter option?



Solution 1:[1]

I think your code is almost good. I see only issue what if key is missing in target dictionary?

def merge_dicts(tgt, enhancer):
    for key, val in enhancer.items():
        if key not in tgt:
            tgt[key] = val
            continue

        if isinstance(val, dict):
            merge_dicts(tgt[key], val)
        else:
            tgt[key] = val
    return tgt

This code, do most of the same what you have written.

  1. check if key present in target dict, if not update regardless type.
  2. if val is dict, then we use recusion
  3. if val is not dict then update from enhancing dict

But I see still one issue what if in target dict value is string and in enhancer value is dict?

enhancer = {
    "key3": {
        "key3_1": "value3_1",
        "key3_2": "value3_2"
    }
}

tgt = {
    "key3": "string_val"
}

Then it depends what do you prefer:

  1. Overwrite string with dict from enhancer:
def merge_dicts(tgt, enhancer):
    for key, val in enhancer.items():
        if key not in tgt:
            tgt[key] = val
            continue

        if isinstance(val, dict):
            if not isinstance(tgt[key], dict):
                tgt[key] = dict()
            merge_dicts(tgt[key], val)
        else:
            tgt[key] = val
    return tgt
  1. Keep string value from target dict:
def merge_dicts(tgt, enhancer):
    for key, val in enhancer.items():
        if key not in tgt:
            tgt[key] = val
            continue

        if isinstance(val, dict):
            if not isinstance(tgt[key], dict):
                continue
            merge_dicts(tgt[key], val)
        else:
            tgt[key] = val
    return tgt

Solution 2:[2]

Another solution:

from copy import deepcopy
from typing import Any


def is_all_dict(a1: Any, a2: Any) -> bool:
    return isinstance(a1, dict) and isinstance(a2, dict)


def recursively_merge(d1: dict, d2: dict) -> dict:
    d = deepcopy(d1)
    for k, v2 in d2.items():
        if (v := d.get(k)) and is_all_dict(v, v2):
            sub_dicts = []
            for sk, sv2 in v2.items():
                if (sv := v.get(sk)) and is_all_dict(sv, sv2):
                    sub_dicts.append((sv, sv2))
                else:
                    v[sk] = sv2
            while sub_dicts:
                sds = []
                for v, v2 in sub_dicts:
                    for sk, sv2 in v2.items():
                        if (sv := v.get(sk)) and is_all_dict(sv, sv2):
                            sds.append((sv, sv2))
                        else:
                            v[sk] = sv2
                sub_dicts = sds
        else:
            d[k] = v2
    return d

Output:

In [26]: import pprint

In [27]: pprint.pprint(recursively_merge(a, b))
{'key1': 'not_key1',
 'key2': 'value2',
 'key3': {'key': 'new key without index?',
          'key3_1': 'Definitely not value3_1',
          'key3_2': 'value3_2'},
 'key4': 'something new'}

Solution 3:[3]

If you want something really shorthand using dictionary comprehensions, you could use the below approach.

NB: By using .get(k) in the if statement, we avoid having to check whether k is in the dictionary

def merge_dicts(d1, d2):
    check = lambda k, v: isinstance(d1.get(k), dict) and isinstance(v, dict)
    return {**d1, **{k: merge_dicts(d1[k], d2[k]) if check(k, v) else v for k, v in d2.items()}}

Output:

>>> from pprint import pprint
>>> pprint(merge_dicts(a,b))
{'key1': 'not_key1',
 'key2': 'value2',
 'key3': {'key': 'new key without index?',
          'key3_1': 'Definitely not value3_1',
          'key3_2': 'value3_2'},
 'key4': 'something new'}

Solution 4:[4]

This is a more simple solution to what you have. I believe you need at least 3.7+

c = {**a, **b}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 marc_s
Solution 2 Waket Zheng
Solution 3
Solution 4 John Stud