How to convert a nested python dictionary into a simple namespace?

Question:

Let’s say I have a nested dictionary with depth N. How can I convert each inner nested dictionary into a simple namespace?

example_input = {key0a: "test", key0b: {key1a: {key2a: {...keyNx}, key2b: "test"} ,key1b: "test"}}

example_output = SimpleNamespace(key0a: "test", key0b: SimpleNamespace(key1a: SimpleNamespace(key2a: SimpleNamespace(...keyNx), key2b: "test"), key1b: "test"))

Are there better alternatives to make the keys of the dictionary accessible per dot notation (e.g. example_input.key0a) if the example_input dict is given – without having external dependencies?

Asked By: BugsBuggy

||

Answers:

Just answering your second (last) question – it is popular topic, there exist many different projects (non-standard) that make your dictionary into dot notation object.

For example this one – attrdict. Install it through pip install attrdict.

Example of usage:

Try it online!

from attrdict import AttrDict
d = {'a': 1, 'b': [{'c': 2}, {'d': {'e': {'f': {5: {'g': 3}}}}}]}
ad = AttrDict(d)
print(ad.b[1].d.e.f(5).g) # 3

If you wonder how module like attrdict is implemented, then I wrote a very simple implementation of similar functionality (of course real attrdict should be more rich):

Try it online!

class AttrD(object):
    def __init__(self, d = {}):
        self.set_d(d)
    def __getattr__(self, key):
        return AttrD(self.get_or_create(key))
    def __setattr__(self, key, value):
        self.set_or_create(key, value)
    def __getitem__(self, key):
        return AttrD(self.get_or_create(key))
    def __setitem__(self, key, value):
        self.set_or_create(key, value)
    def __call__(self, key):
        return AttrD(self.get_or_create(key))
    def __repr__(self):
        return repr(self._d)
    def to_obj(self):
        return self._d
    def set_d(self, d):
        super(AttrD, self).__setattr__('_d', d)
    def get_or_create(self, name):
        if type(self._d) in (dict,) and name not in self._d:
            self._d[name] = {}
        if type(self._d) in (list, tuple) and len(self._d) <= name:
            self.set_d(self._d + type(self._d)(
                [None] * (name + 1 - len(self._d))))
        return self._d[name]
    def set_or_create(self, key, value):
        self.get_or_create(key)
        self._d[key] = value

ad = AttrD({'a': 1, 'b': [{'c': 2}, {'d': {'e': {'f': {5: {'g': 3}}}}}]})
ad.b[1].d.e.f(5).g = [4, 5, 6]
print(ad.b[1].d.e.f(5).g[2]) # 6
print(AttrD({'a': 123}).b.c) # Non-existent defaults to {}
Answered By: Arty

Using recursion

example_input = {'key0a': "test", 'key0b': 
                 {'key1a': {'key2a': 'end', 'key2b': "test"} ,'key1b': "test"}, 
                 "something": "else"}
def parse(d):
  x = SimpleNamespace()
  _ = [setattr(x, k, parse(v)) if isinstance(v, dict) else setattr(x, k, v) for k, v in d.items() ]    
  return x

result = parse(example_input)
print (result)

Output:

namespace(key0a='test', 
          key0b=namespace(key1a=namespace(key2a='end', key2b='test'), key1b='test'), 
          something='else')
Answered By: mujjiga

Based on mujjija’s solution this is what I came up with. Full code below

from types import SimpleNamespace


def parse(data):
    if type(data) is list:
        return list(map(parse, data))
    elif type(data) is dict:
        sns = SimpleNamespace()
        for key, value in data.items():
            setattr(sns, key, parse(value))
        return sns
    else:
        return data


info = {
    'country': 'Australia',
    'number': 1,
    'slangs': [
        'no worries mate',
        'winner winner chicken dinner',
        {
            'no_slangs': [123, {'definately_not': 'hello'}]
        }
    ],
    'tradie': {
        'name': 'Rizza',
        'occupation': 'sparkie'
    }
}

d = parse(info)
assert d.country == 'Australia'
assert d.number == 1
assert d.slangs[0] == 'no worries mate'
assert d.slangs[1] == 'winner winner chicken dinner'
assert d.slangs[2].no_slangs[0] == 123
assert d.slangs[2].no_slangs[1].definately_not == 'hello'
assert d.tradie.name == 'Rizza'
assert d.tradie.occupation == 'sparkie'

If I’m not mistaken, Python doesn’t support Tail Call Optimization. So please be careful when using deep recursive functions in Python. For small examples, it should be fine.

Update

Another version. object_hook does the magic of nesting. I prefer this version because I can directly feed them to the jinja2 template engine.

import json


class _DotDict(dict):
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__


def dot(data=None):
    if data is []:
        return []
    return json.loads(json.dumps(data), object_hook=_DotDict) if data else _DotDict()
Answered By: Eddie

2022 answer: now there is a tiny, relatively fast library I have published, called dotwiz, which alternatively can be used to provide easy dot access for a python dict object.

It should, coincidentally, be a little faster than the other options — I’ve added a quick and dirty benchmark code I put together using the timeit module below, timing against both a attrdict and SimpleNamespace approach — the latter of which actually performs pretty solid in times.

Note that I had to modify the parse function slightly, so that it handles nested dicts within a list object, for example.

from timeit import timeit
from types import SimpleNamespace

from attrdict import AttrDict
from dotwiz import DotWiz


example_input = {'key0a': "test", 'key0b': {'key1a': [{'key2a': 'end', 'key2b': "test"}], 'key1b': "test"},
                 "something": "else"}


def parse(d):
    x = SimpleNamespace()
    _ = [setattr(x, k,
                 parse(v) if isinstance(v, dict)
                 else [parse(e) for e in v] if isinstance(v, list)
                 else v) for k, v in d.items()]
    return x


print('-- Create')
print('attrdict:         ', round(timeit('AttrDict(example_input)', globals=globals()), 2))
print('dotwiz:           ', round(timeit('DotWiz(example_input)', globals=globals()), 2))
print('SimpleNamespace:  ', round(timeit('parse(example_input)', globals=globals()), 2))
print()

dw = DotWiz(example_input)
ns = parse(example_input)
ad = AttrDict(example_input)

print('-- Get')
print('attrdict:         ', round(timeit('ad.key0b.key1a[0].key2a', globals=globals()), 2))
print('dotwiz:           ', round(timeit('dw.key0b.key1a[0].key2a', globals=globals()), 2))
print('SimpleNamespace:  ', round(timeit('ns.key0b.key1a[0].key2a', globals=globals()), 2))
print()

print(ad)
print(dw)
print(ns)

assert ad.key0b.key1a[0].key2a 
       == dw.key0b.key1a[0].key2a 
       == ns.key0b.key1a[0].key2a 
       == 'end'

Here are the results, on my M1 Mac Pro laptop:

attrdict:          0.69
dotwiz:            1.3
SimpleNamespace:   1.38

-- Get
attrdict:          6.06
dotwiz:            0.06
SimpleNamespace:   0.06

The dotwiz library can be installed with pip:

$ pip install dotwiz
Answered By: rv.kvetch

I know its late but stumbled upon similar problem with not easy solution.
I crated this class which can be used same as SimpleNamespace but with recustion built in. Thanks.

from typing import Any
from types import SimpleNamespace as SNS


class RecursiveNS(SNS):
    def __init__(self, **kwargs):
        self.__dict__.update(self.parse(kwargs).__dict__)

    @staticmethod
    def parse(d: dict[str, Any]) -> SNS:
        """Static method that takes dictionary as an argument,
        and returns """
        x = SNS()
        for k, v in d.items():
            setattr(
                x,
                k,
                RecursiveNS.parse(v)
                if isinstance(v, dict)
                else [RecursiveNS.parse(e) for e in v]
                if isinstance(v, list)
                else v,
            )
        return x
Answered By: Rahul
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.