2
\$\begingroup\$

The code below works. The question is if there is a better and more elegant (maintainable) way.

The task is to update a Python dictionary which values are lists with another dictionary with the same structure (values are lists). The usual dict.update() doesn't work because values (the lists) are replaced not updated (via list.extend()).

Example:

a = {'A': [1, 2], 'C': [5]}
b = {'X': [8], 'A': [7, 921]}

Using dict.update() would result in

{'A': [7, 921], 'C': [5], 'X': [8]}

But A should become [1, 2, 7, 921].

Here is my working solution:

#!/usr/bin/env python3
from typing import Any

def update_dict_with_list_values(
    a: dict[Any,list], b: dict[Any,list]) -> dict[Any,list]:
    """Update a dictionary and its list values from another dict."""

    # create a local deep copy
    a = a.copy()

    for key in a:
        try:
            # remove the list
            b_value = b.pop(key)
        except KeyError:
            pass
        else:
            # extend the list
            a[key] = a[key] + b_value

    # add the rest of keys that are not present in the dict to update
    a.update(b)

    return a

if __name__ == '__main__':
    a = {
        'A': [1, 2],
        'C': [5]
    }
    b = {
        'X': [8],
        'A': [7, 921]
    }

    c = update_dict_with_list_values(a, b)

    print(c)  # -> {'A': [1, 2, 7, 921], 'C': [5], 'X': [8]}
\$\endgroup\$

3 Answers 3

8
\$\begingroup\$

Avoid conditionals by using defaultdict. There is only one case: you want to extend-merge lists from the second dictionary, with the target defaulting to an empty list.

from collections import defaultdict
from typing import Any, Iterable


def update_dict_with_list_values(
    a: dict[Any, list], b: dict[Any, Iterable],
) -> dict[Any, list]:
    """Update a dictionary and its list values from another dict."""

    union = defaultdict(list, a)

    for k, values in b.items():
        union[k].extend(values)

    return union

This assumes that you don't mind mutating the value lists of a, which is a risky assumption indeed. To avoid this you would deep-copy a first, via something like

    union = defaultdict(list, (
        (k, list(v)) for k, v in a.items()
    ))

Or, for a very different approach which is naturally immune to side-effect concerns,

return {
    k: a.get(k, []) + b.get(k, [])
    for k in a.keys() | b.keys()
}
\$\endgroup\$
2
  • \$\begingroup\$ Because I don't understand defaultdict at all I don't get its advantage in the current case. \$\endgroup\$
    – buhtz
    Aug 19, 2022 at 5:51
  • 2
    \$\begingroup\$ @buhtz If you don't want defaultdict, you can use method dict.setdefault instead. That is, for a key k in b, you can replace if k in d: a[k].extend(b[k]) else: d[k] = list(b[k]) with the equivalent a.setdefault(k, []).extend(b[k]). \$\endgroup\$
    – Stef
    Aug 19, 2022 at 10:58
5
\$\begingroup\$

Bottom line you need only 2 cases:

  1. if a b key is present in a, merge values (extend for that matter)
  2. if a b key is not present in a, then assign it to a

Assuming you're only expecting lists as values, you could write something like this:

def update_dict_with_list_values(
    a: dict[Any, list[Any]],
    b: dict[Any, list[Any]],
) -> dict[Any, list[Any]]:
    """Update a dictionary and its list values from another dict."""

    for key, value in b.items():
        if key in a:
            a[key].extend(value)
        else:
            a[key] = value

    return a

I would recommend avoiding single-letter variable names!

Hope that helps!

\$\endgroup\$
5
  • \$\begingroup\$ Mhm... The code is easier to read but less efficient because of if statements. I tried to avoid them. And I would hypotize that in a short function like this it is acceptable. \$\endgroup\$
    – buhtz
    Aug 18, 2022 at 19:38
  • 3
    \$\begingroup\$ @buhtz Why do you think it's less efficient? Exception handling tends to be much slower than conditionals. \$\endgroup\$
    – janos
    Aug 18, 2022 at 20:07
  • \$\begingroup\$ But exception only need to be handled when they occur not earlier. \$\endgroup\$
    – buhtz
    Aug 18, 2022 at 20:32
  • \$\begingroup\$ You do not need two cases. \$\endgroup\$
    – Reinderien
    Aug 18, 2022 at 22:54
  • 1
    \$\begingroup\$ Note that a[key] = value should probably be changed into the safer a[key] = list(value), or a[key] = value.copy(), otherwise future modifications ofb might result in accidental modifications of a. \$\endgroup\$
    – Stef
    Aug 19, 2022 at 11:00
1
\$\begingroup\$

Based on that answer I suggest a solution avoiding if. The key point here is that don't iterate over the dict to update (the target) but the dict that is updated with (the source).

def update_dict_with_list_values(
    a: dict[Any,list], b: dict[Any,list]) -> dict[Any,list]:
    """Update a dictionary and its list values from another dict."""

    for key, value in b.items():
        try:
            a[key].extend(value)
        except KeyError:
            a[key] = value

    return a
\$\endgroup\$
5
  • 2
    \$\begingroup\$ The .copy() function of a dict is not a deep copy, it's a shallow copy. \$\endgroup\$
    – janos
    Aug 18, 2022 at 20:10
  • \$\begingroup\$ @RichardNeumann Just a typo. You are also able to edit and correct foreign answers by the way. ;) \$\endgroup\$
    – buhtz
    Aug 19, 2022 at 5:50
  • 1
    \$\begingroup\$ The line a = copy.deepcopy(a) should probably be removed; or if you really want to have it, should should add a warning in the docstring explaining that update_dict_with_list_values will make deepcopies and discard the original objects in a. \$\endgroup\$
    – Stef
    Aug 19, 2022 at 11:02
  • 1
    \$\begingroup\$ You are right. Using a copy would be more a join_dict_with_list_values() then update...(). \$\endgroup\$
    – buhtz
    Aug 19, 2022 at 11:03
  • 1
    \$\begingroup\$ Yes; but even for a join function, you should probably just write a = {k: list(v) for k,v in a.items()} rather than a = copy.deepcopy(a), so as to make copies of the lists without making deepcopies of possible objects in the lists. (Also, in this case I'd recommend calling the return dict c = {k: list(v) for k,v in a.items()} rather than giving it the same name a, which is confusing and hides the fact that it's a local variable) \$\endgroup\$
    – Stef
    Aug 19, 2022 at 11:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.