CollectionsChapter 38 of 114
Nested Dictionaries
Dictionaries inside dictionaries, and how to read them without crashing.
Structure inside structure
A value can be another dictionary, or a list, to any depth. This is the shape JSON arrives in, so you will meet it constantly:
people = {
"ada": {"born": 1815, "fields": ["maths", "computing"]},
"grace": {"born": 1906, "fields": ["computing"]},
}
print(people["ada"]["born"])
print(people["ada"]["fields"][0])Output
1815 maths
Reading safely, one level at a time
Chained [] fails at whichever level is missing, and the KeyError only names that level:
people = {"ada": {"born": 1815}}
try:
print(people["alan"]["born"])
except KeyError as problem:
print("KeyError:", problem)Output
KeyError: 'alan'
Chaining get() is safe but reads badly past two levels:
people = {"ada": {"born": 1815}}
print(people.get("alan", {}).get("born"))
print(people.get("ada", {}).get("born"))Output
None 1815
The {} default is what makes the second get() legal — without it you would be calling get() on None.
A small helper is clearer when the path is long:
def dig(data, *keys, default=None):
for key in keys:
if not isinstance(data, dict) or key not in data:
return default
data = data[key]
return data
people = {"ada": {"address": {"city": "London"}}}
print(dig(people, "ada", "address", "city"))
print(dig(people, "alan", "address", "city", default="unknown"))Output
London unknown
Building nested structures
Assigning into a level that does not exist yet fails:
groups = {}
try:
groups["maths"]["ada"] = True
except KeyError as problem:
print("KeyError:", problem)Output
KeyError: 'maths'
setdefault() creates the level if it is missing and returns it either way:
groups = {}
groups.setdefault("maths", []).append("ada")
groups.setdefault("maths", []).append("grace")
groups.setdefault("computing", []).append("katherine")
print(groups)Output
{'maths': ['ada', 'grace'], 'computing': ['katherine']}defaultdict does the same with less repetition when every value has the same shape:
from collections import defaultdict
groups = defaultdict(list)
groups["maths"].append("ada")
groups["maths"].append("grace")
print(dict(groups))Output
{'maths': ['ada', 'grace']}from collections import defaultdict
groups = defaultdict(list)
print(groups["physics"])
print(dict(groups))Output
[]
{'physics': []}Walking a nested structure
people = {
"ada": {"born": 1815, "fields": ["maths"]},
"grace": {"born": 1906, "fields": ["computing", "navy"]},
}
for name, details in people.items():
fields = ", ".join(details["fields"])
print(f"{name}: born {details['born']}, {fields}")Output
ada: born 1815, maths grace: born 1906, computing, navy
Copying is shallow here too
from copy import deepcopy
original = {"ada": {"fields": ["maths"]}}
shallow = original.copy()
deep = deepcopy(original)
shallow["ada"]["fields"].append("computing")
print(original["ada"]["fields"])
print(deep["ada"]["fields"])Output
['maths', 'computing'] ['maths']
The shallow copy shares the inner dictionary. Only deepcopy gives you a structure you can change without touching the original.
Test yourself
2 questionsWhy does people.get("alan", {}).get("born") not crash?
Show the answer
The {} default means the second get() is called on a dictionary — Without the default the first get() returns None, and None has no get().
What does reading a missing key on a defaultdict do?
Show the answer
Creates the entry with a default value — Merely reading adds the key. Use 'in' to check without creating it.
Dictionary Methods
The full set of dict methods, and which ones change the dictionary.