Unflattening back to arbitrarily nested dictionaries requires that you create new dictionaries for arbitrary paths; c__d
means that there must be a nested dictionary associated with the c
key, so you can set d
in there. c__d__e
means c
is a dictionary with a d
key pointing to another dictionary, were e
is set, etc. Essentially, everything but the last element in key.split('__')
is a key in a dictionary pointing to another dictionary, and you need to create those dictionaries if they have not yet been created, in a loop.
You can use functools.reduce()
to do this, see this previous answer of mine for an explanation of the get_nested_default()
and set_nested()
functions:
from functools import reduce
def get_nested_default(d, path):
return reduce(lambda d, k: d.setdefault(k, {}), path, d)
def set_nested(d, path, value):
get_nested_default(d, path[:-1])[path[-1]] = value
def unflatten(d, separator='__'):
output = {}
for k, v in d.items():
path = k.split(separator)
set_nested(output, path, v)
return output
get_nested_default()
takes care of creating those nested dictionaries if they don't yet exist, for any number of keys. set_nested()
takes care of separating out the last key in a path from the remainder, and setting the value in the innermost dictionary.
This assumes that the input never used __
in keys originally, of course. But as long as you pick a path separator that's not used as a substring of any of the keys in the input dictionary, you can un-flatten back to the original form.
Demo on your example:
>>> unflatten({'a': 'b', 'c__d': 'g'})
{'a': 'b', 'c': {'d': 'g'}}
and a more complex one using a different separator:
>>> from pprint import pprint
>>> flat = {
... 'foo::bar::baz': 'Eric Idle',
... 'foo::bar::spam': 'John Cleese',
... 'foo::spam::ham': 'Terry Gilliam',
... 'spam::ham': 'Vikings singing'
... }
>>> nested = unflatten(flat, '::')
>>> pprint(nested)
{'foo': {'bar': {'baz': 'Eric Idle', 'spam': 'John Cleese'},
'spam': {'ham': 'Terry Gilliam'}},
'spam': {'ham': 'Vikings singing'}}