6

Let's say i have a list of keys

key_lst = ["key1", "key2", "key3"]

and i have a value

value = "my_value"

and an example dict my_dict with this structure

{
"key1": {
    "key2": {
        "key3": "some_value"
        }
    },
}

How can I dynamically assign the new value in variable value to my_dict["key1"]["key2"]["key3"] by going thru / looping over my key_lst?

I can not just say my_dict["key1"]["key2"]["key3"] = value since the keys and the number of keys is changing. I always get the keys (the path that i have to save the value at) in a list...

The output I am looking for is {'key1': {'key2': {'key3': 'my_value'}}}. The dictionary structure is predefined.

I'm using Python 3.7

jpp
  • 159,742
  • 34
  • 281
  • 339
Endogen
  • 589
  • 2
  • 12
  • 24
  • Is this really a duplicate? The code to answer is in the other thread, but conceptually I think the two questions are different. This is about *setting* a nested value while the link is focused on *getting* a nested value – Addison Klinke Jun 15 '22 at 19:32

5 Answers5

17

Predefined dictionary structure: functools.reduce

You can define a function using functools.reduce to apply getitem repeatedly and then set a supplied value:

from functools import reduce
from operator import getitem

def set_nested_item(dataDict, mapList, val):
    """Set item in nested dictionary"""
    reduce(getitem, mapList[:-1], dataDict)[mapList[-1]] = val
    return dataDict

key_lst = ["key1", "key2", "key3"]
value = "my_value"
d = {"key1": {"key2": {"key3": "some_value"}}}

d = set_nested_item(d, key_lst, value)

print(d)
# {'key1': {'key2': {'key3': 'my_value'}}}

Note operator.getitem is used to access dict.__getitem__, or its more commonly used syntactic sugar dict[]. In this instance, functools.reduce calls getitem recursively on dataDict, successively using each value in mapList[:-1] as an argument. With [:-1], we intentionally leave out the last value, so we can use __setitem__ via dict[key] = value for the final key.


Arbitrary dictionary nesting: collections.defaultdict

If you wish to add items at arbitrary branches not yet been defined, you can construct a defaultdict. For this, you can first defaultify your regular dictionary input, then use set_nested_item as before:

from collections import defaultdict

def dd_rec():
    return defaultdict(dd_rec)

def defaultify(d):
    if not isinstance(d, dict):
        return d
    return defaultdict(dd_rec, {k: defaultify(v) for k, v in d.items()})

dd = defaultify(d)

key_lst = ["key1", "key2", "key5", "key6"]
value = "my_value2"
dd = set_nested_item(dd, key_lst, value)

print(dd)

# defaultdict(<function __main__.<lambda>>,
#             {'key1': defaultdict(<function __main__.<lambda>>,
#                          {'key2': defaultdict(<function __main__.<lambda>>,
#                                       {'key3': 'my_value',
#                                        'key5': defaultdict(<function __main__.<lambda>>,
#                                                    {'key6': 'my_value2'})})})})
jpp
  • 159,742
  • 34
  • 281
  • 339
  • 1
    This is a pretty cool answer. Mind explaining a bit more on your `set_nested_item` function for me? – hqkhan Jan 10 '19 at 23:12
  • 2
    But this assumes the particular structure already exists. What about setting new values? You will need a defaultdict, right? This cannot handle KeyErrors as it is. – cs95 Jan 10 '19 at 23:13
  • @hqkhan, Added an explanation. – jpp Jan 10 '19 at 23:15
  • @coldspeed, Agreed, this doesn't handle `KeyError`. That said, not sure whether this is relevant for OP's requirements. For example, it's conceivable that the dictionary structure is *given* and you only want to set values; hence `KeyError` is what you *want*. – jpp Jan 10 '19 at 23:16
  • 1
    JESUS CHRIST! That's a nice solution. Dict structure is actually given and i would expect a `KeyError` if the key is not in there. BTW: Not a requirement but this is more than three times faster than @coldspeed's answer. This is the one, thanks. – Endogen Jan 10 '19 at 23:39
  • I'm not sure it's necessary to return `d` from `set_nested_item()` - the dictionary gets modified in-place already – Addison Klinke Jun 15 '22 at 17:48
  • For clarification, I believe `dd_rec` stands for "defaultdict recursive". The code is a bit odd to read, but it's the same concept from this [answer](https://stackoverflow.com/a/8702435/7446465) – Addison Klinke Jun 15 '22 at 17:54
6

You can iteratively build/access levels using setdefault in a loop:

d = {}
d2 = d
for k in key_lst[:-1]:
    d2 = d2.setdefault(k, {})

d2[key_lst[-1]] = value
print(d)
# {'key1': {'key2': {'key3': 'my_value'}}}

d is the reference to your dictionary, and d2 is a throw-away reference that accesses inner levels at each iteration.

cs95
  • 379,657
  • 97
  • 704
  • 746
1

This is what you want:

def update(d, key_lst , val):
    for k in key_lst[:-1]:
        if k not in d:
            d[k] = {}
        d = d[k]
    d[key_lst[-1]] = val

d = {}

update(d, list('qwer'), 0)
# d = {'q': {'w': {'e': {'r': 0}}}}

You could use defaultdict too, it's neat in a sense but prints rather ugly...:

from collections import defaultdict

nest = lambda: defaultdict(nest)
d = nest()

def update(d, key_lst , val):
    for k in key_lst[:-1]:
        d = d[k]
    d[key_lst[-1]] = val

update(d, 'qwer', 0)
Julien
  • 13,986
  • 5
  • 29
  • 53
0

I guess you can loop through your keys like this :

d = {}
a = d
for i in key_lst: 
    a[i] = {}
    if i == key_lst[-1]:
        a[i] = value
    else:
        a = a[i]
print(d)
# {'key1': {'key2': {'key3': 'my_value'}}}

Edit: I guess I misread the question and answered as if the dictionnary wasn't already existing. jpp answer is pretty neat otherwise I guess!

mgc
  • 5,223
  • 1
  • 24
  • 37
0
key_lst = ["key1", "key2", "key3"]
my_dict={
"key1": {
    "key2": {
        "key3": "some_value"
        }
    },
}

val=my_dict
#loop gets second to last key in chain(path) and assigns it to val
for x in key_lst[:-1]:
    val=val[x]
#now we can update value of last key, cause dictionary key is passed by reference
val[key_lst[-1]]="new value"

print (my_dict)

#{'key1': {'key2': {'key3': 'new value'}}}
Mehrdad Dowlatabadi
  • 1,335
  • 2
  • 9
  • 11