Dynamically index into Python dictionary based on function parameters

Question:

I’m building a function that indexes into a Python dictionary nested inside a list. The user knows what the dictionary looks like in advance. This is what it looks like so far:

def dict_idx(arr: list, subkey: str) -> list:
    for i in arr:
        i[subkey] = i[subkey].replace("_", " ")
    return arr

I would like to make the subkey parameter a list of strings, and to index into the dictionary depending like this. So for instance, if I pass this function a subkey value of [‘user’,’location’], it would index into the array like follows:

    for i in arr:
        i['user']['location'] = i['user']['location'].replace("_", " ")

Is this possible (short of converting the list into a string of dictionary indexes and running eval() on that string)?

Asked By: snark17

||

Answers:

You can loop over all except one of the keys (subkey[:-1]), to access deeper and deeper sub-dictionaries, then index using the last key (subkey[-1]) for the assignment statement at the end.

Something like this:

def dict_idx(arr: list, subkey: list):
    for d in arr:
        for key in subkey[:-1]:
            d = d[key]
        lastkey = subkey[-1]
        d[lastkey] = d[lastkey].replace("_", " ")


arr = [{'user': {'location': 'some_where'},
        'foo': 2},
       {'user': {'location': 'some_where_else'},
        'bar': 3}]

subkey = ['user', 'location']

dict_idx(arr, subkey)

print(arr)

gives:

[{'user': {'location': 'some where'}, 'foo': 2}, {'user': {'location': 'some where else'}, 'bar': 3}]

Note that the function in this suggested code does not return the list, simply modifies it in place (or more specifically, modifies elements of inner data items). It is generally a bad idea for a function to both do an in-place modification and also return the value, because it can lead to confusion as to the purpose of the function.


By the way, you might also want to consider what should happen if the dictionaries do not contain the relevant keys. In the above code, it would raise an KeyError exception. If you would prefer to silently ignore these errors, you could put the code inside the outer for loop into tryexcept block, for example as follows:

def dict_idx(arr: list, subkey: list):
    for d in arr:
        try:
            for key in subkey[:-1]:
                d = d[key]
            lastkey = subkey[-1]
            d[lastkey] = d[lastkey].replace("_", " ")
        except KeyError:
            continue
Answered By: alani

This is the same approach as alani, although, with some style differences I would humbly suggest:

from operator import getitem
from functools import reduce

def retrieve_nested(dictionary, keys):
    return reduce(getitem, keys, dictionary)

def dict_idx(data: list[dict], subkeys: list[str]) -> None:
    *subkeys, lastkey = subkeys
    for d in data:
        value = retrieve_nested(d, subkeys)
        value[lastkey] = value[lastkey].replace("_", " ")

Or perhaps, dispense with the helper function:

from operator import getitem
from functools import reduce


def dict_idx(data: list[dict], subkeys: list[str]) -> None:
    *subkeys, lastkey = subkeys
    for d in data:
        value = reduce(getitem, d, subkeys)
        value[lastkey] = value[lastkey].replace("_", " ")
Answered By: juanpa.arrivillaga
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.