4

I'd like to be able to set a nested dictionary item using a single dot-delimited string path. For example, given the following dictionary:

data_dictionary = {
  "user_data": {
    "first_name": "Some",
    "last_name": "Guy",
    "phone": "212-111-1234"
  }
}

I'd like to be able to set a specific item using a single string, something like this:

data_dictionary['user_data.phone'] = '818-333-4567'

Anyone know of a library or simple technique for accomplishing something like this?

Gatmando
  • 2,179
  • 3
  • 29
  • 56
  • 1
    If you're constructing the data_dictionary yourself, you could just have unpack the nested dictionaries into the outer dict with those keys. – kubatucka Oct 14 '21 at 14:10
  • You could write a function to split the string and recursively lookup the keys. – Peter Wood Oct 14 '21 at 14:13
  • Parse the path & split it by `.`. then traverse the dict using the components till the penultimate component. Finally do a standard assignment using the final key. – rdas Oct 14 '21 at 14:14
  • You might want to have a look at this post, https://stackoverflow.com/a/28463329/4985099 – sushanth Oct 14 '21 at 14:17
  • Using a dataclass for your data might be a good choice, if you want attribute access (dot notation). – Jussi Nurminen Oct 14 '21 at 14:33

2 Answers2

6

You could define a little helper function:

def set(obj, path, value):
    *path, last = path.split(".")
    for bit in path:
        obj = obj.setdefault(bit, {})
    obj[last] = value

set(data_dictionary, "user_data.phone", "123")
data_dictionary
# {'user_data': {'first_name': 'Some', 'last_name': 'Guy', 'phone': '123'}}

You can also subclass dict and override __setitem__:

class my_dict(dict):
  def __setitem__(self, item, value):
    if "." in item:
      head, path = item.split(".", 1)
      obj = self.setdefault(head, my_dict())
      obj[path] = value
    else:
      super().__setitem__(item, value)

dd = my_dict({
    "user_data": {
        "first_name": "Some",
        "last_name": "Guy",
        "phone": "212-111-1234"
    }
})

dd["user_data.phone"]  = "123"
dd
# {'user_data': {'first_name': 'Some', 'last_name': 'Guy', 'phone': '123'}}
user2390182
  • 72,016
  • 6
  • 67
  • 89
  • Your initial 'set' function works exactly as I had hoped so thanks for that! I did actually prefer your suggestion of the my_dict subclass (it feels cleaner to me) but it broke down on a nested dictionary when I tried to add a further "address": {"line1": "xxx", "line2": "yyy"}. I've been playing with it to try and make it work. I'll update if I have time to fix. – Gatmando Oct 14 '21 at 15:34
  • this is great! I'm getting an error with `*path, last = path.split(".")` though saying `SyntaxError: ("required (...)+ loop did not match anything at input '*'", ('', 2, 4, ' *path, last = path.split(".")\n'))`. Any ideas? – njminchin Mar 29 '22 at 21:26
0

I suggest something more friendly for nested structures such as Box

Then you can just use dot notation to access as many nests as you want:

from box import Box

data = Box(data_dictionary)
data.user_data.phone = '818-333-4567'
print(data.user_data.phone)

818-333-4567
Jab
  • 26,853
  • 21
  • 75
  • 114
  • 1
    I see why, I must have mis-interpreted the question. I have edited with a more appropriate answer. – Jab Oct 14 '21 at 14:23