Nested Data Structures
A filing cabinet has drawers. Each drawer has folders. Each folder has pages. To find one fact, you go drawer, folder, page.
Real information rarely fits in one flat list. A class has students. Each student has a name, a grade level, and several quiz scores. Each quiz belongs to a subject. You've learned four kinds of collections: lists, tuples, dictionaries, and sets. The real power comes from putting them inside each other.
A filing cabinet
Think of a school office's filing cabinet. The cabinet has a drawer for each grade level. Inside each drawer is a folder for each student. Inside each folder are pages: a report card, an ID photo, a list of clubs.
To find Maria's second quiz score, you'd open the grade 11 drawer, pull out Maria's folder, find the scores page, and read the second line. Nested data works exactly like that: you go one level at a time, from the outside in.
A list of dictionaries
The most common shape of all: a list of records, where each record is a dictionary. Each dictionary describes one thing, and the list holds them all in order.
students = [
{"name": "Maria", "grade": 11, "scores": [88, 94, 83]},
{"name": "Ben", "grade": 10, "scores": [72, 80]},
{"name": "Carlo", "grade": 11, "scores": [95, 90, 98]},
]
print(len(students))
print(students[0]["name"])
print(students[2]["scores"][1])3
Maria
90Long nested structures are easier to read spread over several lines, one record per line, like above. Python doesn't mind the line breaks inside brackets, and the comma after the last item is allowed, which makes adding another line easy.
Read students[2]["scores"][1] from left to right, one step at a time, like opening the filing cabinet:
students[2]is the third dictionary: Carlo's record.["scores"]is the value for the"scores"key in it: the list[95, 90, 98].[1]is the second item in that list:90.
Looping through records
A for loop hands you one dictionary at a time, and you use its keys inside the loop:
students = [
{"name": "Maria", "grade": 11, "scores": [88, 94, 83]},
{"name": "Ben", "grade": 10, "scores": [72, 80]},
{"name": "Carlo", "grade": 11, "scores": [95, 90, 98]},
]
for student in students:
average = sum(student["scores"]) / len(student["scores"])
print(f"{student['name']} (grade {student['grade']}): {average:.1f}")Maria (grade 11): 88.3
Ben (grade 10): 76.0
Carlo (grade 11): 94.3Notice student['name'] uses single quotes inside the f-string, because the f-string itself uses double quotes.
This shape, a list of dictionaries, is also exactly what data looks like when it comes from files and websites. You'll see it again in Working with JSON.
A dictionary of lists
Sometimes it's more natural to group things under a key. Each club has a list of members:
clubs = {
"chess": ["Ana", "Ben"],
"robotics": ["Carlo", "Dina", "Eli"],
}
clubs["chess"].append("Fe")
clubs["drama"] = []
for club, members in clubs.items():
print(f"{club}: {len(members)} members")chess: 3 members
robotics: 3 members
drama: 0 membersclubs["chess"] is a list, so you can call list methods like append right on it.
A dictionary of dictionaries
When each item has a unique name, you can look records up directly by that name, instead of searching a list:
inventory = {
"notebook": {"price": 45, "stock": 120},
"pencil": {"price": 10, "stock": 0},
}
print(inventory["notebook"]["price"])
inventory["pencil"]["stock"] += 50
print(inventory["pencil"])45
{'price': 10, 'stock': 50}Choosing a shape
There's no single right answer, but here's a rule of thumb:
- List of dictionaries: a collection of similar records, where order matters or you go through them all. Every student in the class.
- Dictionary of dictionaries: records you look up by a unique name or ID. The product called "notebook."
- Dictionary of lists: groups of items under a label. The members of each club.
Missing keys, deep down
The deeper the nesting, the more places something can be missing. If one record doesn't have a key, record["key"] crashes with a KeyError. get from Dictionaries helps here too:
students = [
{"name": "Maria", "email": "maria@example.com"},
{"name": "Ben"},
]
for student in students:
email = student.get("email", "(no email)")
print(student["name"], "->", email)Maria -> maria@example.com
Ben -> (no email)Copies of nested data
One warning. .copy(), from Lists, copies only the outer layer. The inner lists and dictionaries are still shared:
original = [{"name": "Maria", "scores": [88]}]
copy = original.copy()
copy[0]["scores"].append(100)
print(original)[{'name': 'Maria', 'scores': [88, 100]}]The copy is a new list, but it holds the same dictionary, so changing Maria's scores through copy changed them in original too. When you need a fully separate copy of nested data, use copy.deepcopy from the copy module:
import copy
original = [{"name": "Maria", "scores": [88]}]
separate = copy.deepcopy(original)
separate[0]["scores"].append(100)
print(original)[{'name': 'Maria', 'scores': [88]}]Try it
This program keeps a small gradebook as a list of dictionaries. It adds a score, adds a student, finds the top student, and groups students by grade level. Predict every line.
setdefault(key, []) gives back the list for that key, first creating an empty one if the key is new. It's a handy shortcut for building a dictionary of lists.
Running Python here
Running Python right in the browser is coming to Syntaxia soon. For now, you can read and edit the code here. If you have Python installed on your computer (see Setting Up), save it as main.py and run it with python main.py (or python3 main.py on macOS and Linux).
Check your prediction
Maria 88.3
Ben 81.0
Carlo 94.3
Dina 85.0
Top student: Carlo
{11: ['Maria', 'Carlo'], 10: ['Ben', 'Dina']}
Total quizzes taken: 10Ben's new 91 raises his average to 81.0. max(students, key=average) compares the students by calling average on each one. The grade groups appear in the order each grade was first seen: 11 (from Maria), then 10 (from Ben).
Try it yourself
- Add a
"clubs"list to Maria's dictionary, and print her clubs. - Print only the students in grade 11 whose average is at least 90.
- Rebuild the gradebook as a dictionary of dictionaries, keyed by name, and print Carlo's scores with a single lookup.
Check your understanding
Up next
That completes the Data Structures chapter. Dictionaries have been doing a great job of describing students, but a student can also do things: take a quiz, get an average, print a report card. Next, you'll bundle data and actions together, in Classes and Objects.
