21

I am trying to convert a list of lists data structure to a dictionary of dictionaries.

The list is defined as follows:

l = [
  ['PP','Ear-rings', 'Holesovice', 2000],
  ['PP','Skirts', 'Holesovice', 1000],
  ['PP','Dresses', 'E-shop', 1500],
  ['BM','Butterfly', 'Holesovice', 1600]
]

My aim is to have the dictionary structure as follows:

#{'PP' : {'Holesovice' : {'Ear-rings' : 2000, 'Skirts' : 1000},
#         'E-shop' : {'Dresses' : 1500}},
# 'BM' : {'Holesovice' : {'Butterfly' : 1600}}
#}

This bit of code does not return desired output:

labels_d = {}
items_d = {}
shops_d = {}

for index, row in enumerate(l):
  items_d[row[1]] = row[3]
  shops_d[row[2]] = items_d
  labels_d[row[0]] = shops_d

print(labels_d)

I found some posts that deal with converting lists to dictionaries here and here but I did not make it work the way I want. Is there any 'clean' way how to achieve the structure posted above?

jpp
  • 159,742
  • 34
  • 281
  • 339
New2coding
  • 715
  • 11
  • 23

5 Answers5

27

Using dict.setdefault(key, {}) is a good way to approach the creation of nested dictionaries of fixed depth.

l = [
  ['PP','Ear-rings', 'Holesovice', 2000],
  ['PP','Skirts', 'Holesovice', 1000],
  ['PP','Dresses', 'E-shop', 1500],
  ['BM','Butterfly', 'Holesovice', 1600]
]

d = {}

for tag, item, source, qty in l:
    d.setdefault(tag, {}).setdefault(source, {})[item] = qty 

Output

{'BM': {'Holesovice': {'Butterfly': 1600}},
 'PP': {'E-shop': {'Dresses': 1500},
        'Holesovice': {'Ear-rings': 2000, 'Skirts': 1000}}}

Generalization

The above solution can be made more general by building a class of nested dictionary, dropping the requirements to have a fixed depth.

class NestedDict(dict):
    def __getitem__(self, item):
        if item not in self:
            self[item] = NestedDict()
        return super().__getitem__(item)

d = NestedDict()

for tag, item, source, qty in l:
    d[tag][source][item] = qty 

Also notice that the class approach is created so it only creates an object if the key does not exist while the setdefault approach created an empty dict on every access.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • 4
    It really grinds something in the back of my head to see a new object created every time you call `setdefault`, although this is a very nice solution. – Mad Physicist Sep 05 '18 at 14:09
  • @OlivierMelançon They just mean that `NestedDict()` is called whether or not `item` already has a value, which is a little inelegant. – Patrick Haugh Sep 05 '18 at 14:47
  • 1
    @MadPhysicist this is solved in the general approach – Olivier Melançon Sep 05 '18 at 15:47
  • 1
    I'm aware of the solution and I'm sure you are too. I liked your original answer for it's simplicity. My comment was just a general dissatisfaction with `setdefault`. It would be nice to have another method that accepts a callable instead of a preconstructed instance. – Mad Physicist Sep 05 '18 at 18:47
  • @MadPhysicist I agree, having setdefault to take a callable would be way better in my opinion as well – Olivier Melançon Sep 06 '18 at 01:45
  • 1
    I've never posted anything on the Python discussion thread before. Maybe now would be a good time to start. I'll see if I can come up with a simple implementation first. – Mad Physicist Sep 06 '18 at 02:08
17

You can use the infinitely nested defaultdict trick:

from collections import defaultdict

def nested_dict():
    return defaultdict(nested_dict)

nd = nested_dict()
for a, b, c, d in l:
    nd[a][c][b] = d
user2390182
  • 72,016
  • 6
  • 67
  • 89
  • I love this as well. I feel it would be somewhat neater if you made nested_dict a class instead of a factory. – Olivier Melançon Sep 05 '18 at 14:14
  • 1
    @OlivierMelançon Certainly a challenge to have the recursive logic in a class in such a tidy way. After all, the argument to the `defaultdict` constructor is the default *factory* ;-) – user2390182 Sep 05 '18 at 14:25
  • 1
    @schwobaseggl it's fairly simple really since the factory is stored as an instance argument. `class NestedDict(defaultdict): def __init__(self): self.default_factory = NestedDict` – Olivier Melançon Sep 05 '18 at 14:29
  • @OlivierMelançon True, I like that as well. It certainly provides an additional insight. – user2390182 Sep 05 '18 at 14:35
  • 1
    This answer is, of course, good. BUT I do think it's important to note the difference between this and my very similar but more precise answer. When you check a key `nd['key1']['key2']['key3']` and it doesn't exist you will get an empty dictionary `{}`, while in my solution you get `0`. It may be relevant as conceptually you may not *expect* `{}`. – jpp Sep 05 '18 at 16:10
  • @jpp Valid, good point that needs to be taken into account when dealing with this data structure. The main advantage of this approach over yours is its ability to deal with arbitrarily (unknown) deep nesting. – user2390182 Sep 05 '18 at 16:19
6

You can use collections.defaultdict and iterate. In this case, you can define precisely a nested dictionary to reflect your data structure.

from collections import defaultdict

L = [['PP','Ear-rings', 'Holesovice', 2000],
     ['PP','Skirts', 'Holesovice', 1000],
     ['PP','Dresses', 'E-shop', 1500],
     ['BM','Butterfly', 'Holesovice', 1600]]

d = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))

for code, item, shop, value in L:
    d[code][shop][item] = value

Result

defaultdict({'BM': defaultdict({'Holesovice': defaultdict(int, {'Butterfly': 1600})}),
             'PP': defaultdict({'E-shop': defaultdict(int, {'Dresses': 1500}),
                                'Holesovice': defaultdict(int,
                                {'Ear-rings': 2000, 'Skirts': 1000})})})
jpp
  • 159,742
  • 34
  • 281
  • 339
0
def toNested1(l):
    def addKeyDict(map,key):    
        if key not in map:
            item = map[key] = {}
            return item            
        return map[key]

    zz = {}
    for a0,a1,a2,a3 in l :
        addKeyDict( addKeyDict( zz, a0) , a2 )[a1] = a3
    return zz
napuzba
  • 6,033
  • 3
  • 21
  • 32
0

Here post a pretty straightforward way to compose a new dictionary:

If the items in each row of list not in the corresponding depth of dictionary, just add/append the key-value pair to the dict.

code:

list = [
    ['PP','Ear-rings', 'Holesovice', 2000],
    ['PP','Skirts', 'Holesovice', 1000],
    ['PP','Dresses', 'E-shop', 1500],
    ['BM','Butterfly', 'Holesovice', 1600]
]

dicta = {}
for row in list:
    if row[0] not in dicta.keys():
        dicta[row[0]] = {row[2]:{row[1]:row[3]}}
        continue
    if row[2] not in dicta[row[0]].keys():
        dicta[row[0]][row[2]] = {row[1]:row[3]}
        continue
    if row[1] not in dicta[row[0]][row[2]].keys():
        dicta[row[0]][row[2]][row[1]] = row[3]

print(dicta)

output:

{'BM': {'Holesovice': {'Butterfly': 1600}},
 'PP': {'E-shop': {'Dresses': 1500},
        'Holesovice': {'Ear-rings': 2000, 'Skirts': 1000}}}