Search code examples
pythonvariablesobject

Elegant way to check if a nested key exists in a dict?


Is there are more readable way to check if a key buried in a dict exists without checking each level independently?

Lets say I need to get this value in a object buried (example taken from Wikidata):

x = s['mainsnak']['datavalue']['value']['numeric-id']

To make sure that this does not end with a runtime error it is necessary to either check every level like so:

if 'mainsnak' in s and 'datavalue' in s['mainsnak'] and 'value' in s['mainsnak']['datavalue'] and 'nurmeric-id' in s['mainsnak']['datavalue']['value']:
    x = s['mainsnak']['datavalue']['value']['numeric-id']

The other way I can think of to solve this is wrap this into a try catch construct which I feel is also rather awkward for such a simple task.

I am looking for something like:

x = exists(s['mainsnak']['datavalue']['value']['numeric-id'])

which returns True if all levels exists.


Solution

  • To be brief, with Python you must trust it is easier to ask for forgiveness than permission

    try:
        x = s['mainsnak']['datavalue']['value']['numeric-id']
    except KeyError:
        pass
    

    The answer

    Here is how I deal with nested dict keys:

    def keys_exists(element, *keys):
        '''
        Check if *keys (nested) exists in `element` (dict).
        '''
        if not isinstance(element, dict):
            raise AttributeError('keys_exists() expects dict as first argument.')
        if len(keys) == 0:
            raise AttributeError('keys_exists() expects at least two arguments, one given.')
    
        _element = element
        for key in keys:
            try:
                _element = _element[key]
            except KeyError:
                return False
        return True
    

    Example:

    data = {
        "spam": {
            "egg": {
                "bacon": "Well..",
                "sausages": "Spam egg sausages and spam",
                "spam": "does not have much spam in it"
            }
        }
    }
    
    print 'spam (exists): {}'.format(keys_exists(data, "spam"))
    print 'spam > bacon (do not exists): {}'.format(keys_exists(data, "spam", "bacon"))
    print 'spam > egg (exists): {}'.format(keys_exists(data, "spam", "egg"))
    print 'spam > egg > bacon (exists): {}'.format(keys_exists(data, "spam", "egg", "bacon"))
    

    Output:

    spam (exists): True
    spam > bacon (do not exists): False
    spam > egg (exists): True
    spam > egg > bacon (exists): True
    

    It loop in given element testing each key in given order.

    I prefere this to all variable.get('key', {}) methods I found because it follows EAFP.

    Function except to be called like: keys_exists(dict_element_to_test, 'key_level_0', 'key_level_1', 'key_level_n', ..). At least two arguments are required, the element and one key, but you can add how many keys you want.

    If you need to use kind of map, you can do something like:

    expected_keys = ['spam', 'egg', 'bacon']
    keys_exists(data, *expected_keys)