Elegant way to remove fields from nested dictionaries

Viewed 53081

I had to remove some fields from a dictionary, the keys for those fields are on a list. So I wrote this function:

def delete_keys_from_dict(dict_del, lst_keys):
    """
    Delete the keys present in lst_keys from the dictionary.
    Loops recursively over nested dictionaries.
    """
    dict_foo = dict_del.copy()  #Used as iterator to avoid the 'DictionaryHasChanged' error
    for field in dict_foo.keys():
        if field in lst_keys:
            del dict_del[field]
        if type(dict_foo[field]) == dict:
            delete_keys_from_dict(dict_del[field], lst_keys)
    return dict_del

This code works, but it's not very elegant and I'm sure that there is a better solution.

11 Answers
def delete_keys_from_dict(d, to_delete):
    if isinstance(to_delete, str):
        to_delete = [to_delete]
    if isinstance(d, dict):
        for single_to_delete in set(to_delete):
            if single_to_delete in d:
                del d[single_to_delete]
        for k, v in d.items():
            delete_keys_from_dict(v, to_delete)
    elif isinstance(d, list):
        for i in d:
            delete_keys_from_dict(i, to_delete)

d = {'a': 10, 'b': [{'c': 10, 'd': 10, 'a': 10}, {'a': 10}], 'c': 1 }
delete_keys_from_dict(d, ['a', 'c']) # inplace deletion 
print(d)

>>> {'b': [{'d': 10}, {}]}

This solution works for dict and list in a given nested dict. The input to_delete can be a list of str to be deleted or a single str.

Plese note, that if you remove the only key in a dict, you will get an empty dict.

this works with dicts containing Iterables (list, ...) that may contain dict. Python 3. For Python 2 unicode should also be excluded from the iteration. Also there may be some iterables that don't work that I'm not aware of. (i.e. will lead to inifinite recursion)

from collections.abc import Iterable

def deep_omit(d, keys):
    if isinstance(d, dict):
        for k in keys:
            d.pop(k, None)
        for v in d.values():
            deep_omit(v, keys)
    elif isinstance(d, Iterable) and not isinstance(d, str):
        for e in d:
            deep_omit(e, keys)

    return d

If you have nested keys as well and based on @John La Rooy's answer here is an elegant solution:

from boltons.iterutils import remap


def sof_solution():
    data = {"user": {"name": "test", "pwd": "******"}, "accounts": ["1", "2"]}
    sensitive = {"user.pwd", "accounts"}

    clean = remap(
        data,
        visit=lambda path, key, value: drop_keys(path, key, value, sensitive)
    )
    print(clean)


def drop_keys(path, key, value, sensitive):
    if len(path) > 0:
        nested_key = f"{'.'.join(path)}.{key}"
        return nested_key not in sensitive
    return key not in sensitive

sof_solution() # prints {'user': {'name': 'test'}}

Since nobody posted an interactive version that could be useful for someone:

def delete_key_from_dict(adict, key):
    stack = [adict]
    while stack:
        elem = stack.pop()
        if isinstance(elem, dict):
            if key in elem:
                del elem[key]
            for k in elem:
                stack.append(elem[k])

This version is probably what you would push to production. The recursive version is elegant and easy to write but it scales badly (by default Python uses a maximum recursion depth of 1000).

I came here to search for a solution to remove keys from deeply nested Python3 dicts and all solutions seem to be somewhat complex.

Here's a oneliner for removing keys from nested or flat dicts:

nested_dict = {
    "foo": {
        "bar": {
            "foobar": {},
            "shmoobar": {}
        }
    }
}

>>> {'foo': {'bar': {'foobar': {}, 'shmoobar': {}}}}

nested_dict.get("foo", {}).get("bar", {}).pop("shmoobar", None)

>>> {'foo': {'bar': {'foobar': {}}}}

I used .get() to not get KeyError and I also provide empty dict as default value up to the end of the chain. I do pop() for the last element and I provide None as the default there to avoid KeyError.

Related