15

Many SO posts show you how to efficiently check the existence of a key in a dictionary, e.g., Check if a given key already exists in a dictionary

How do I do this for a multi level key? For example, if d["a"]["b"] is a dict, how can I check if d["a"]["b"]["c"]["d"] exists without doing something horrendous like this:

if "a" in d and isInstance(d["a"], dict) and "b" in d["a"] and isInstance(d["a"]["b"], dict) and ...

Is there some syntax like

if "a"/"b"/"c"/"d" in d

What I am actually using this for: we have jsons, parsed into dicts using simplejson, that I need to extract values from. Some of these values are nested three and four levels deep; but sometimes the value doesn't exist at all. So I wanted something like:

val = None if not d["a"]["b"]["c"]["d"] else  d["a"]["b"]["c"]["d"] #here d["a"]["b"] may not even exist

EDIT: prefer not to crash if some subkey exists but is not a dictionary, e.g, d["a"]["b"] = 5.

Community
  • 1
  • 1
Tommy
  • 12,588
  • 14
  • 59
  • 110
  • This isn't a base feature of the language as there is no way to add new syntax. You could define a new class that overrides the __contains__ function which is called by the "x in y" expression. Do you want efficiency of syntax or execution? They may not be the same thing. – mobiusklein Nov 18 '14 at 22:30
  • Well, my goal was efficient syntax, but this was under the assumption that the O(1) dictionary lookup time would be preserved. I realize, however, that raising exceptions is expensive, so perhaps this is more involved than simply checking key existence. – Tommy Nov 21 '14 at 03:13
  • Exception handling won't be the costly part. What you want fundamentally isn't in the language as utdemir pointed out. Meitham's answer is as close as you're getting to what you want without doing a lot more work, defining a class as I mentioned before, and then going through the trouble to make `simplejson` unpack objects into that and not vanilla dictionaries. – mobiusklein Nov 21 '14 at 05:12

5 Answers5

23

Sadly, there isn't any builtin syntax or a common library to query dictionaries like that.

However, I believe the simplest(and I think it's efficient enough) thing you can do is:

d.get("a", {}).get("b", {}).get("c")

Edit: It's not very common, but there is: https://github.com/akesterson/dpath-python

Edit 2: Examples:

>>> d = {"a": {"b": {}}}
>>> d.get("a", {}).get("b", {}).get("c")
>>> d = {"a": {}}
>>> d.get("a", {}).get("b", {}).get("c")
>>> d = {"a": {"b": {"c": 4}}}
>>> d.get("a", {}).get("b", {}).get("c")
4
utdemir
  • 26,532
  • 10
  • 62
  • 81
  • d.get? And does this return a boolean or the item? – Tommy Nov 17 '14 at 18:13
  • From `help({}.get)`: `D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.`. It returns the item if it's found, `None` if it isn't. – utdemir Nov 17 '14 at 18:15
  • This does not seem to work. If d["a"]["b"] is a string, instead of a dict, it bombs with "str object has no method get" on the .get(c) part. – Tommy Nov 17 '14 at 22:20
  • 1
    Well, you stated that `if d["a"]["b"]["c"] is a dict`, so my code is assuming they are dicts. – utdemir Nov 18 '14 at 17:31
  • 3
    Then it doesn't work. WHat if ["c"] wasn't defined yet? The point of the question was to see whether a multi level dict key is valid or not. It could be off by multiple levels; as indicated by by `isInstance` nonsense. – Tommy Nov 18 '14 at 20:56
  • If one of the subkeys can be a type other than a `dict` clarify your question. If they are either `dict`s or not exist, my solution works. – utdemir Nov 18 '14 at 22:22
  • 1
    Can't believe this answer had a negative score. I think it's brilliant. – lucasnadalutti Oct 20 '15 at 13:55
  • @lucasnadalutti it had a negative score because if any of the subkeys is anything other than a dictionary, it crashes. – Tommy Feb 05 '16 at 01:59
3

This isn't probably a good idea and I wouldn't recommend using this in prod. However, if you're just doing it for learning purposes then the below might work for you.

def rget(dct, keys, default=None):
    """
    >>> rget({'a': 1}, ['a'])
    1
    >>> rget({'a': {'b': 2}}, ['a', 'b'])
    2
    """
    key = keys.pop(0)
    try:
        elem = dct[key]
    except KeyError:
        return default
    except TypeError:
        # you gotta handle non dict types here
        # beware of sequences when your keys are integers
    if not keys:
        return elem
    return rget(elem, keys, default)
Meitham
  • 9,178
  • 5
  • 34
  • 45
2

UPDATE: I ended up writing my own open-source, pippable library that allows one to do this: https://pypi.python.org/pypi/dictsearch

Tommy
  • 12,588
  • 14
  • 59
  • 110
0

A non-recursive version, quite similar to @Meitham's solution, which does not mutate the looked-for key. Returns True/False if the exact structure is present in the source dictionary.

def subkey_in_dict(dct, subkey):
    """ Returns True if the given subkey is present within the structure of the source dictionary, False otherwise.
    The format of the subkey is parent_key:sub_key1:sub_sub_key2 (etc.) - description of the dict structure, where the
    character ":" is the delemiter.

    :param dct: the dictionary to be searched in.
    :param subkey: the target keys structure, which should be present.
    :returns Boolean: is the keys structure present in dct.
    :raises AttributeError: if subkey is not a string.
    """
    keys = subkey.split(':')
    work_dict = dct
    while keys:
        target = keys.pop(0)
        if isinstance(work_dict, dict):
            if target in work_dict:
                if not keys:    # this is the last element in the input, and it is in the dict
                    return True
                else:   # not the last element of subkey, change the temp var
                    work_dict = work_dict[target]
            else:
                return False
        else:
            return False

The structure that is checked is in the form parent_key:sub_key1:sub_sub_key2, where the : char is the delimiter. Obviously - it will match case-sensitively, and will stop (return False) if there's a list within the dictionary.

Sample usage:

dct = {'a': {'b': {'c': {'d': 123}}}}

print(subkey_in_dict(dct, 'a:b:c:d'))    # prints True
print(subkey_in_dict(dct, 'a:b:c:d:e'))  # False
print(subkey_in_dict(dct, 'a:b:d'))      # False
print(subkey_in_dict(dct, 'a:b:c'))      # True
Todor Minakov
  • 19,097
  • 3
  • 55
  • 60
0

This is what I usually use

def key_in_dict(_dict: dict, key_lookup: str, separator='.'):
    """
        Searches for a nested key in a dictionary and returns its value, or None if nothing was found.
        key_lookup must be a string where each key is deparated by a given "separator" character, which by default is a dot
    """
    keys = key_lookup.split(separator)
    subdict = _dict

    for k in keys:
        subdict = subdict[k] if k in subdict else None
        if subdict is None: break

    return subdict

Returns the key if exists, or None it it doesn't

key_in_dict({'test': {'test': 'found'}}, 'test.test') // 'found'
key_in_dict({'test': {'test': 'found'}}, 'test.not_a_key') // None
mijorus
  • 111
  • 1
  • 7