17

Given the following dictionary:

d = {"a":{"b":{"c":"winning!"}}}

I have this string (from an external source, and I can't change this metaphor).

k = "a.b.c"

I need to determine if the dictionary has the key 'c', so I can add it if it doesn't.

This works swimmingly for retrieving a dot notation value:

reduce(dict.get, key.split("."), d)

but I can't figure out how to 'reduce' a has_key check or anything like that.

My ultimate problem is this: given "a.b.c.d.e", I need to create all the elements necessary in the dictionary, but not stomp them if they already exist.

6
  • 4
    Yes, just accept your own answers to your questions. That's perfectly OK (even desired, so people who have the same question will see that there exists an answer). Commented Sep 13, 2012 at 21:22
  • By any chance is the source actually Cocoa KVC? And, if so, can you just use PyObjC instead of pure Python? (Besides being simpler, that would also guarantee that the keys mean exactly the same thing they're intended to, even if there are bugs in other people's code…) Commented Sep 13, 2012 at 21:53
  • What is so special in using dot notation 'as a string' to search through dictionary? One has to use some additional function for it. What about to search multi-dimentional dicts JS style, without any strings x = conf.a.b.c. I don't know if it is possible in Python. But using a string for it is not cool Commented Feb 13, 2013 at 16:53
  • That 'reduce' fragment helped me. Thanks! Commented Apr 21, 2015 at 19:52
  • Near-duplicate, although from 2018 and presumably you asked about Python 2.x not 3.x Set Python dict items recursively, when given a compound key 'foo.bar.baz' Commented Jan 6, 2019 at 10:03

5 Answers 5

33

You could use an infinite, nested defaultdict:

>>> from collections import defaultdict
>>> infinitedict = lambda: defaultdict(infinitedict)
>>> d = infinitedict()
>>> d['key1']['key2']['key3']['key4']['key5'] = 'test'
>>> d['key1']['key2']['key3']['key4']['key5']
'test'

Given your dotted string, here's what you can do:

>>> import operator
>>> keys = "a.b.c".split(".")
>>> lastplace = reduce(operator.getitem, keys[:-1], d)
>>> lastplace.has_key(keys[-1])
False

You can set a value:

>>> lastplace[keys[-1]] = "something"
>>> reduce(operator.getitem, keys, d)
'something'
>>> d['a']['b']['c']
'something'
Sign up to request clarification or add additional context in comments.

1 Comment

That's deep. infinitedict = lambda: defaultdict(infinitedict) +1
18

... or using recursion:

def put(d, keys, item):
    if "." in keys:
        key, rest = keys.split(".", 1)
        if key not in d:
            d[key] = {}
        put(d[key], rest, item)
    else:
        d[keys] = item

def get(d, keys):
    if "." in keys:
        key, rest = keys.split(".", 1)
        return get(d[key], rest)
    else:
        return d[keys]

4 Comments

This could be improved by calling split only once and passing the resulting list to the recursive function.
This one almost has it. Problem is, put doesn't account for if a key does exist, but is a text value that needs overwriting with a dict.
I used this with modifications. Very similar to @l4mpi's answer.
Thanks, this is the only solution that worked for my use case that I've been struggling with all day!
4

How about an iterative approach?

def create_keys(d, keys):
    for k in keys.split("."):
        if not k in d: d[k] = {}  #if the key isn't there yet add it to d
        d = d[k]                  #go one level down and repeat

If you need the last key value to map to anything else than a dictionary you could pass the value as an additional argument and set this after the loop:

def create_keys(d, keys, value):
    keys = keys.split(".")
    for k in keys[:-1]:
        if not k in d: d[k] = {}
        d = d[k]            
    d[keys[-1]] = value

Comments

1

I thought this discussion was very useful, but for my purpose to only get a value (not setting it), I ran into issues when a key was not present. So, just to add my flair to the options, you can use reduce in combination of an adjusted dict.get() to accommodate the scenario that the key is not present, and then return None:

from functools import reduce
import re
from typing import Any, Optional

def find_key(dot_notation_path: str, payload: dict) -> Any:
    """Try to get a deep value from a dict based on a dot-notation"""

    def get_despite_none(payload: Optional[dict], key: str) -> Any:
        """Try to get value from dict, even if dict is None"""
        if not payload or not isinstance(payload, (dict, list)):
            return None
        # can also access lists if needed, e.g., if key is '[1]'
        if (num_key := re.match(r"^\[(\d+)\]$", key)) is not None:
            try:
                return payload[int(num_key.group(1))]
            except IndexError:
                return None
        else:
            return payload.get(key, None)

    found = reduce(get_despite_none, dot_notation_path.split("."), payload)
   
    # compare to None, as the key could exist and be empty
    if found is None:
        raise KeyError()
    return found

In my use case, I need to find a key within an HTTP request payload, which can often include lists as well. The following examples work:

payload = {
    "haystack1": {
        "haystack2": {
            "haystack3": None, 
            "haystack4": "needle"
        }
    },
    "haystack5": [
        {"haystack6": None}, 
        {"haystack7": "needle"}
    ],
    "haystack8": {},
}

find_key("haystack1.haystack2.haystack4", payload)
# "needle"
find_key("haystack5.[1].haystack7", payload)
# "needle"
find_key("[0].haystack5.[1].haystack7", [payload, None])
# "needle"
find_key("haystack8", payload)
# {}
find_key("haystack1.haystack2.haystack4.haystack99", payload)
# KeyError

EDIT: added list accessor

Comments

0
d = {"a":{}}
k = "a.b.c".split(".")

def f(d, i):
    if i >= len(k):
        return "winning!"
    c = k[i]
    d[c] = f(d.get(c, {}), i + 1)
    return d

print f(d, 0)
"{'a': {'b': {'c': 'winning!'}}}"

2 Comments

I feel sorry for your coworkers having to maintain this kind of code. What do d, k, f, i and c even mean?
d,k, and c are as per the question. f is a function, i is an index

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.