Introducing deepmerge. It's a library designed to provide simple controls around a merging system for basic Python data structures like dicts and lists.

It provides a few common cases for merging (like always merge + override, or raise an exception):

from deepmerge import always_merger, merge_or_raise

base = {
    "a": ["b"],
    "c": 1,
    "nested": {
        "nested_dict": "value",
        "nested_list": ["a"]
    }
}

nxt = {
    "new_key": "new_value",
    "nested": {
        "nested_dict": "new_value",
        "nested_list": ["b"],
        "new_nested_key": "value"
    }
}

always_merge(base, nxt)
assert base == {
      "a": ["b"],
      "c": 1,
      "new_key": "new_value"
      "nested": {
          "nested_dict": "new_value",
          "nested_list": ["a", "b"],
          "new_nested_key": "value"
      }
}

deepmerge allows customization as well, for when you want to specify the merging strategy:

from deepmerge import Merger

my_merger = Merger(
    # pass in a list of tuples,with the
    # strategies you are looking to apply
    # to each type.
    [
        (list, ["prepend"]),
        (dict, ["merge"])
    ],
    # next, choose the fallback strategies,
    # applied to all other types:
    ["override"],
    # finally, choose the strategies in
    # the case where the types conflict:
    ["override"]
)
base = {"foo": ["bar"]}
next = {"bar": "baz"}
my_merger.merge(base, next)
assert base == {"foo": ["bar"], "bar": "baz"}

For each strategy choice, pass in a list of strings specifying built in strategies, or a function defining your own:

def merge_sets(merger, path, base, nxt):
    base |= nxt
    return base

def merge_list(merger, path, base, nxt):
    if len(nxt) > 0:
        base.append(nxt[-1])
        return base

return Merger(
    [
        (list, merge_list),
        (dict, "merge"),
        (set, merge_sets)
    ],
    [],
    [],
)

That's it! Give and try, and Pull Requests are always encouraged.