1

I have an array of complex dict that have some value as a string "NULL" and I want to remove, my dict looks like this:

d = [{
  "key1": "value1",
  "key2": {
    "key3": "value3",
    "key4": "NULL",
    "z": {
       "z1": "NULL",
       "z2": "zzz",
    },
  },
  "key5": "NULL"
}, {
  "KEY": "NULL",
  "AAA": "BBB",
}]

And I want to wipe out all keys that have NULL as value

Like this:

[{"key1": "value1", "key2": {"key3": "value3", "z": {"z2": "zzz"}}}, {"AAA": "BBB"}]

I am using Python 3.9, so it is possible to use walrus operator.

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
Rodrigo
  • 135
  • 4
  • 45
  • 107

5 Answers5

6

Here is how you can do this with recursion:

def remove_null(d):
    if isinstance(d, list):
        for i in d:
            remove_null(i)
    elif isinstance(d, dict):
        for k, v in d.copy().items():
            if v == 'NULL':
                d.pop(k)
            else:
                remove_null(v)

d = [{
  "key1": "value1",
  "key2": {
    "key3": "value3",
    "key4": "NULL",
    "z": {
       "z1": "NULL",
       "z2": "zzz",
    },
  },
  "key5": "NULL"
}, {
  "KEY": "NULL",
  "AAA": "BBB",
}]

remove_null(d)
print(d)

Output:

[{"key1": "value1", "key2": {"key3": "value3", "z": {"z2": "zzz"}}}, {"AAA": "BBB"}]
Red
  • 26,798
  • 7
  • 36
  • 58
  • Why not `del d[k]` instead of `d.pop(k)`? The `pop()` method returns a result that you're ignoring, so why ask Python to do the extra work of returning that result? – cdlane Jun 14 '21 at 04:51
2

Since your asking for a canonical answer, here's an approach that avoids editing the provided object in-place, provides type hints, has a docstring, can be invoked with the remove parameter to remove different values, and errors with a TypeError as appropriate:

from collections import abc
from typing import Dict, List, Union

def without_nulls(d: Union[List, Dict], remove: tuple = ('NULL', 'null')) -> Union[List, Dict]:
    """ Given a list or dict, returns an object of the same structure without null dict values. """
    collection_types = (list, tuple) # avoid including 'str' here
    mapping_types = (abc.Mapping,)
    all_supported_types = (*mapping_types, *collection_types)
    if isinstance(d, collection_types):
        return [without_nulls(x, remove) if isinstance(x, all_supported_types) else x for x in d]
    elif isinstance(d, mapping_types):
        clean_val = lambda x: without_nulls(x, remove) if isinstance(x, all_supported_types) else x
        return {k: clean_val(v) for k, v in d.items() if v not in remove}
    raise TypeError(f"Unsupported type '{type(d)}': {d!r}")

Usage:

>>> cleaned_d = without_nulls(d)
>>> cleaned_d
[{'key1': 'value1', 'key2': {'key3': 'value3', 'z': {'z2': 'zzz'}}}, {'AAA': 'BBB'}]
>>> d # d is unchanged
[{'key1': 'value1', 'key2': {'key3': 'value3', 'key4': 'NULL', 'z': {'z1': 'NULL', 'z2': 'zzz'}}, 'key5': 'NULL'}, {'KEY': 'NULL', 'AAA': 'BBB'}]

If I wanted to have it remove None instead of 'NULL', I could call it like this:

without_nulls(d, remove=(None,))

Here's an even more generalized version - the sort of thing I'd expect to find in a general-purpose Python toolkit:

from collections import abc
from typing import Any, Callable, Dict, List, Optional, Union

def filter_dicts(d: Union[List, Dict],
                 value_filter: Optional[Callable[[Any], bool]] = None) -> Union[List, Dict]:
    """
    Given a list or dict, returns an object of the same structure without filtered values.

    By default, key-value pairs where the value is 'None' are removed. The `value_filter` param
    must be a function which takes values from the provided dict/list structure, and returns a
    truthy value if the key-value pair is to be removed, and a falsey value otherwise.
    """
    collection_types = (list, tuple) # avoid including 'str' here
    mapping_types = (abc.Mapping,)
    all_supported_types = (*mapping_types, *collection_types)
    if value_filter is None:
        value_filter = lambda x: x is None
    if isinstance(d, collection_types):
        return [filter_dicts(x, value_filter) if isinstance(x, all_supported_types) else x for x in d]
    elif isinstance(d, mapping_types):
        clean_val = lambda x: filter_dicts(x, value_filter) if isinstance(x, all_supported_types) else x
        return {k: clean_val(v) for k, v in d.items() if not value_filter(v)}
    raise TypeError(f"Unsupported type '{type(d)}': {d!r}")

We can use it like this:

>>> filter_dicts(d)
[{'key1': 'value1', 'key2': {'key3': 'value3', 'key4': 'NULL', 'z': {'z1': 'NULL', 'z2': 'zzz'}}, 'key5': 'NULL'}, {'KEY': 'NULL', 'AAA': 'BBB'}]
>>> filter_dicts(d) == d
True
>>> filter_dicts(d, lambda x: x in (None, 'NULL', 'null'))
[{'key1': 'value1', 'key2': {'key3': 'value3', 'z': {'z2': 'zzz'}}}, {'AAA': 'BBB'}]

The benefit of this version is that it allows the caller to completely customize what criteria is employed when removing values. The function handles traversing the structure, and building up a new one to be returned, while the caller gets to decide what gets removed.

Will Da Silva
  • 6,386
  • 2
  • 27
  • 52
1

Solution:

d = [{
  "key1": "value1",
  "key2": {
    "key3": "value3",
    "key4": "NULL",
    "z": {
       "z1": "NULL",
       "z2": "zzz",
    },
  },
  "key5": "NULL"
}, {
  "KEY": "NULL",
  "AAA": "BBB",
}]



for element in list(d):
    for key, value in element.copy().items():
        if value == "NULL":
            element.pop(key, None)
        elif isinstance(value, dict):
            for inner_key, inner_value in value.copy().items():
                if inner_value == "NULL":
                    value.pop(inner_key, None)
                elif isinstance(inner_value, dict):
                    for nested_inner_key, nested_inner_value in inner_value.copy().items():
                        if nested_inner_value == "NULL":
                            inner_value.pop(nested_inner_key, None)
print(d)

Output:

[{'key1': 'value1', 'key2': {'key3': 'value3', 'z': {'z2': 'zzz'}}}, {'AAA': 'BBB'}]

Doing .copy() of each dictionary / nested dictionary, else you'll end up with this error.

Check out the same here also.

surya
  • 719
  • 5
  • 13
  • This may work for this specific dictionary, but the OP might have other dictionaries that are nested even more, hence their request of recursion in the title. – Red Jun 08 '21 at 19:22
1

Maybe something like:

def remove_nulls(d):
    res = {}
    for k, v in d.items():
        if v == "NULL":
            continue
        if isinstance(v, dict):
            res[k] = remove_nulls(v)
        else:
            res[k] = v
    return res


print([remove_nulls(d) for d in lt])
funnydman
  • 9,083
  • 4
  • 40
  • 55
0

I am using Python 3.9, so it is possible to use walrus operator.

Well then, let's throw one in there since no one else did:

from collections import deque

def remove_null(thing):
    if isinstance(thing, list):
        deque(map(remove_null, thing), maxlen=0)
    elif isinstance(thing, dict):
        for key in thing.copy():
            if (value := thing[key]) == 'NULL':
                del thing[key]
            else:
                remove_null(value)

Note that I'm using map instead of a loop to show an alternative to the other answers. However, this creates the same situation that I commented on @AnnZen about, using an operation that returns a result and then ignoring it. To force the map lazy evaluation, I use an empty deque to consume it.

cdlane
  • 40,441
  • 5
  • 32
  • 81