Lists inside lists, dictionaries inside lists, lists inside dictionaries — how Python represents real, structured information, and how to read it one level at a time
So far, each collection you have worked with has held simple values: a list of numbers, a dictionary mapping names to scores, a set of unique words. But real programs rarely deal with data this flat. A gradebook does not just hold a list of names. It holds names alongside scores, assignments, and averages. A contact book does not just hold phone numbers. It holds records with names, numbers, addresses, and notes. A simple database does not hold single values. It holds rows, each of which has multiple fields.
Python lets you build these richer structures by putting collections inside other collections. A list can contain other lists. A dictionary can map keys to lists. A list can hold dictionaries. These are called nested data structures.
This chapter teaches you how to build nested structures, read them, update them, loop through them, and reason about them carefully. Nested structures are powerful, but they also require patience. A small confusion about which level you are working at can produce bugs that are hard to trace. You will learn to think in levels, to draw diagrams, and to write code that handles nested data clearly.
By the end of this chapter, you should be able to:
setdefault() to avoid key errors.setdefault()Think about how you might describe a single student in a gradebook. You need a name. You need a list of quiz scores. You might need an average. You might need a letter grade. No single Python type holds all of that naturally. A string holds the name. A list holds the scores. A float holds the average. A dictionary, however, can hold all of these pieces together under one label.
student = {
"name": "Amara",
"scores": [88, 92, 79, 95],
"average": 88.5,
"grade": "B+"
}
Here, the dictionary student contains a list as the value for the key "scores". That list is nested inside the dictionary.
Now imagine you have thirty students. You could put each student dictionary into a list.
gradebook = [
{"name": "Amara", "scores": [88, 92, 79, 95]},
{"name": "Ben", "scores": [74, 81, 68, 72]},
{"name": "Celia", "scores": [95, 99, 97, 100]}
]
This is a list of dictionaries. The outer structure is a list. Each item inside is a dictionary. Each dictionary contains a list.
Three levels. All standard Python. All readable. Nested data structures are not a new feature. They are just collections used as values inside other collections. You already know lists and dictionaries. This chapter teaches you to use them together.
Flat data becomes limiting quickly. Consider storing the names of students who passed a course. A list works fine.
passed = ["Amara", "Celia", "Devon"]
Now consider storing each student's name and their final score together. A flat list cannot do this cleanly.
# This is confusing and fragile
passed = ["Amara", 91.0, "Celia", 98.0, "Devon", 84.5]
A list of dictionaries expresses the same information far more clearly.
passed = [
{"name": "Amara", "final": 91.0},
{"name": "Celia", "final": 98.0},
{"name": "Devon", "final": 84.5}
]
Any time you find yourself storing related pieces of data alongside each other in a flat list, that is usually a signal that nesting is a better choice.
A nested list is a list that contains other lists as its elements.
rows = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
Here, rows is a list with three elements. Each element is itself a list with three integers. To access the outer list:
rows[0] # [1, 2, 3]
rows[1] # [4, 5, 6]
rows[2] # [7, 8, 9]
To access an element inside an inner list, use two indexes. The first index selects the inner list. The second index selects the element within it.
rows[0][0] # 1
rows[0][2] # 3
rows[1][1] # 5
rows[2][0] # 7
Read rows[1][1] as: go to rows, take item at index 1 (which is [4, 5, 6]), then take the item at index 1 from that list (which is 5).
Trace this step by step.
data = [
[10, 20, 30],
[40, 50, 60]
]
x = data[0][2]
y = data[1][0]
| Expression | Step 1 | Step 2 | Result |
|---|---|---|---|
data[0][2] | data[0] → [10, 20, 30] | [10, 20, 30][2] | 30 |
data[1][0] | data[1] → [40, 50, 60] | [40, 50, 60][0] | 40 |
So x is 30 and y is 40.
You can update elements at any level using the same two-index syntax.
data = [[1, 2], [3, 4]]
data[0][1] = 99
print(data) # [[1, 99], [3, 4]]
You can also append to an inner list.
data[1].append(5)
print(data) # [[1, 99], [3, 4, 5]]
A two-dimensional list — a list of lists where every inner list has the same length — is a natural way to represent a table.
scores = [
[85, 90, 78],
[92, 88, 95],
[70, 74, 81]
]
Think of this as three rows and three columns. Each inner list is one row. Each index position within a row is one column.
| Row Index | Column 0 | Column 1 | Column 2 |
|---|---|---|---|
| 0 | 85 | 90 | 78 |
| 1 | 92 | 88 | 95 |
| 2 | 70 | 74 | 81 |
To get the value in row 1, column 2:
scores[1][2] # 95
A nested loop over the rows and columns lets you print a table:
scores = [
[85, 90, 78],
[92, 88, 95],
[70, 74, 81]
]
for row in scores:
for value in row:
print(value, end="\t")
print()
85 90 78 92 88 95 70 74 81
The end="\t" argument prints a tab instead of a newline after each number. The bare print() at the end of each row starts a new line.
scores = [
[85, 90, 78],
[92, 88, 95],
[70, 74, 81]
]
for i, row in enumerate(scores):
total = sum(row)
average = total / len(row)
print(f"Student {i}: total = {total}, average = {average:.1f}")
Student 0: total = 253, average = 84.3 Student 1: total = 275, average = 91.7 Student 2: total = 225, average = 75.0
sum(row) works because each row is an ordinary list of numbers. Nesting does not break the tools you already know.
A list of dictionaries stores a sequence of records, where each record is a dictionary.
students = [
{"name": "Amara", "grade": "A", "score": 95},
{"name": "Ben", "grade": "C", "score": 72},
{"name": "Celia", "grade": "A", "score": 98}
]
Each item in students is a dictionary with the same three keys. The list holds them in order.
students[0] # {"name": "Amara", "grade": "A", "score": 95}
students[0]["name"] # "Amara"
students[1]["score"] # 72
students[2]["grade"] # "A"
Read students[1]["score"] as: go to students, take item at index 1 (Ben's record), then look up key "score" in that dictionary (which is 72).
for student in students:
print(f"{student['name']}: {student['grade']} ({student['score']})")
Amara: A (95) Ben: C (72) Celia: A (98)
Each iteration gives you one dictionary. You can access any key inside that dictionary normally.
def find_student(students, name):
for student in students:
if student["name"] == name:
return student
return None
result = find_student(students, "Ben")
if result:
print(result["score"]) # 72
# Add a new student
students.append({"name": "Devon", "grade": "B", "score": 84})
# Update an existing student's score
students[1]["score"] = 75
students[1]["grade"] = "C+"
A dictionary of lists maps each key to a list of values. This structure is useful when one key is associated with multiple values — for example, a course roster where each teacher is associated with a list of students, or a schedule where each day has a list of events.
roster = {
"Monday": ["Intro Python", "Web Dev", "Data Structures"],
"Wednesday": ["Intro Python", "Algorithms"],
"Friday": ["Web Dev", "Databases", "Capstone Lab"]
}
roster["Monday"] # ["Intro Python", "Web Dev", "Data Structures"]
roster["Monday"][0] # "Intro Python"
roster["Friday"][2] # "Capstone Lab"
for day, courses in roster.items():
print(f"{day}:")
for course in courses:
print(f" - {course}")
Monday: - Intro Python - Web Dev - Data Structures Wednesday: - Intro Python - Algorithms Friday: - Web Dev - Databases - Capstone Lab
roster["Wednesday"].append("Office Hours")
If a key might not exist yet, use setdefault() to create an empty list first.
assignments = {}
assignments.setdefault("Amara", []).append("Lab 1")
assignments.setdefault("Amara", []).append("Lab 2")
assignments.setdefault("Ben", []).append("Lab 1")
print(assignments)
# {"Amara": ["Lab 1", "Lab 2"], "Ben": ["Lab 1"]}
setdefault(key, default) returns the existing value if the key exists, or sets the key to default and returns it. This avoids a KeyError when appending to a key that does not yet exist.
A record is a dictionary that represents a single real-world object or entity, with each key describing one attribute of that object.
book = {
"title": "The Hitchhiker's Guide to the Galaxy",
"author": "Douglas Adams",
"year": 1979,
"pages": 193,
"in_stock": True
}
Records are one of the most practical patterns in Python. They let you group related data under a single name without defining a class.
A record's values can themselves be collections.
employee = {
"name": "Rosa Ferreira",
"department": "Engineering",
"skills": ["Python", "SQL", "Linux"],
"contact": {
"email": "[email protected]",
"phone": "555-0184"
}
}
Here, "skills" maps to a list and "contact" maps to another dictionary. Accessing nested values:
employee["name"] # "Rosa Ferreira"
employee["skills"][0] # "Python"
employee["contact"]["email"] # "[email protected]"
Each step drills one level deeper.
In practice, you usually have a list of records.
employees = [
{
"name": "Rosa Ferreira",
"department": "Engineering",
"skills": ["Python", "SQL", "Linux"]
},
{
"name": "James Okafor",
"department": "Marketing",
"skills": ["Excel", "Analytics", "Copywriting"]
}
]
for emp in employees:
skill_list = ", ".join(emp["skills"])
print(f"{emp['name']} ({emp['department']}): {skill_list}")
Rosa Ferreira (Engineering): Python, SQL, Linux James Okafor (Marketing): Excel, Analytics, Copywriting
When you loop through a nested structure, you often need a loop inside a loop. The outer loop iterates over the outer collection. The inner loop iterates over each inner collection.
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
for row in matrix:
for value in row:
print(value, end=" ")
print()
1 2 3 4 5 6 7 8 9
Trace this carefully.
data = [[10, 20], [30, 40], [50, 60]]
total = 0
for row in data:
for value in row:
total += value
print(total)
| Outer Loop: row | Inner Loop: value | total After Step |
|---|---|---|
[10, 20] | 10 | 10 |
[10, 20] | 20 | 30 |
[30, 40] | 30 | 60 |
[30, 40] | 40 | 100 |
[50, 60] | 50 | 150 |
[50, 60] | 60 | 210 |
Final output: 210.
courses = [
{"title": "Python 101", "students": ["Amara", "Ben", "Celia"]},
{"title": "Python 201", "students": ["Devon", "Elena"]}
]
for course in courses:
print(f"Course: {course['title']}")
for student in course["students"]:
print(f" {student}")
Course: Python 101 Amara Ben Celia Course: Python 201 Devon Elena
The outer loop iterates over course records. The inner loop iterates over the list of students inside each record.
Nested structures can be confusing until you slow down and read them level by level. A useful technique: draw a box diagram before writing any code.
For a list of dictionaries:
catalog → list
item 0 → dict
"title" → "Python 101"
"price" → 49.99
"tags" → list
item 0 → "beginner"
item 1 → "programming"
item 1 → dict
"title" → "Data Science Basics"
"price" → 59.99
"tags" → list
item 0 → "data"
item 1 → "intermediate"
Given this structure, how would you print the first tag of the second course? Think step by step:
catalog — the whole listcatalog[1] — the second dictionarycatalog[1]["tags"] — the list of tagscatalog[1]["tags"][0] — the first tagcatalog = [
{"title": "Python 101", "price": 49.99, "tags": ["beginner", "programming"]},
{"title": "Data Science Basics","price": 59.99, "tags": ["data", "intermediate"]}
]
print(catalog[1]["tags"][0]) # "data"
When accessing deeply nested values, read the expression from left to right, one bracket pair at a time.
catalog[1]["tags"][0]
catalog — the list[1] — take the second item (a dictionary)["tags"] — look up the "tags" key in that dictionary (a list)[0] — take the first item of that list (a string)If you get an error, check whether each step is giving you what you expect. Print intermediate values.
print(catalog[1]) # the dictionary
print(catalog[1]["tags"]) # the list
print(catalog[1]["tags"][0]) # the first tag
When you are not sure what a nested expression gives you, print it before using it. This takes thirty seconds and saves you from fifteen minutes of confusing errors.
Before writing nested data structure code, it helps to decide the shape of your data. Ask yourself:
Requirements: store multiple contacts, each with a name, phone number, and list of labels. Start with the shape:
contacts → list of records each record → dict with "name" (string), "phone" (string), "labels" (list of strings)
Code:
contacts = [
{"name": "Amara Diallo", "phone": "555-0100", "labels": ["friend", "classmate"]},
{"name": "Ben Nakamura", "phone": "555-0181", "labels": ["family"]},
{"name": "Celia Reyes", "phone": "555-0234", "labels": ["friend", "study group"]}
]
This design is clean. Each record is a dictionary. The "labels" field holds a list because one contact can have multiple labels.
| Use Case | Outer Structure | Each Item |
|---|---|---|
| Sequence of simple values | list | string, number |
| Sequence of records | list | dict |
| Key → one value | dict | string, number |
| Key → multiple values | dict | list |
| Key → structured record | dict | dict |
| Table (rows and columns) | list | list |
There is rarely one right answer. Choose the shape that makes your code readable and your access patterns natural.
Nested structures are powerful. They can also produce code that is hard to read, hard to debug, and easy to get wrong. Here are guidelines for keeping nested code manageable.
Instead of:
print(roster["Monday"][0])
Consider:
monday_courses = roster["Monday"]
first_course = monday_courses[0]
print(first_course)
This is longer, but each step is clear. When something goes wrong, you can print monday_courses and see exactly what you are working with.
Instead of embedding complex nested access inside other logic:
for student in gradebook:
if student["scores"][2] > 90:
print(student["name"])
Write a helper that makes the intent clear:
def third_score(student):
return student["scores"][2]
for student in gradebook:
if third_score(student) > 90:
print(student["name"])
Most programs need at most two or three levels of nesting. If you find yourself writing data[a][b][c][d], the data structure may need to be redesigned. A structure that goes four levels deep is not wrong by definition, but it is a warning sign to step back and ask whether there is a cleaner shape.
When you are not sure what a nested expression gives you, print it before using it.
print(type(students[0])) # <class 'dict'>
print(students[0].keys()) # dict_keys(['name', 'grade', 'score'])
print(students[0]["name"]) # "Amara"
matrix = [[1, 2], [3, 4]]
print(matrix[0]) # [1, 2] — this is the inner list, not a value
print(matrix[0][1]) # 2 — this is a value
record = {"name": "Amara", "score": 95}
print(record[0]) # KeyError: 0
# A dictionary uses keys, not integer indexes
scores = [88, 92, 79]
print(scores["first"]) # TypeError
# A list uses integer indexes, not string keys
gradebook = [{"name": "Amara", "scores": [88, 92]}]
gradebook["scores"].append(79) # TypeError: list indices must be integers
# You must access the dictionary first:
gradebook[0]["scores"].append(79)
sum() still work on each row.setdefault(key, default) method safely creates a list for a new key before appending to it.Answer these questions before beginning the full review questions. They check basic understanding, not deep analysis.
data[0] and data[0][0] when data is a list of lists?setdefault() solve when building a dictionary of lists?data[0][2] or catalog[1]["tags"]. Skipping a level produces the wrong value or an error.Read each code block and answer the questions without running the code.
data = [
[5, 10, 15],
[20, 25, 30],
[35, 40, 45]
]
data[0]?data[2][1]?data[1][0]?len(data)?len(data[0])?library = [
{"title": "Dune", "author": "Herbert", "year": 1965},
{"title": "Neuromancer", "author": "Gibson", "year": 1984},
{"title": "Ender's Game", "author": "Card", "year": 1985}
]
library[1] return?library[0]["author"] return?library[2]["year"] return?"Neuromancer" using indexes and keys?len(library) return?schedule = {
"Mon": ["Math", "Science", "English"],
"Wed": ["History", "PE"],
"Fri": ["Math", "Art", "Science"]
}
schedule["Mon"] return?schedule["Fri"][1] return?len(schedule["Wed"]) return?"Science" from the "Fri" list?person = {
"name": "Jordan Kim",
"age": 22,
"hobbies": ["hiking", "reading", "coding"],
"address": {
"city": "Portland",
"state": "OR"
}
}
person["hobbies"][2] return?person["address"]["city"] return?len(person["hobbies"]) return?Trace each program by hand and write the output.
rows = [[1, 2], [3, 4], [5, 6]]
for row in rows:
print(row[0] + row[1])
records = [
{"name": "Ana", "score": 88},
{"name": "Bo", "score": 74},
{"name": "Cara", "score": 95}
]
for r in records:
if r["score"] >= 90:
print(r["name"])
totals = {}
scores = [
{"subject": "Math", "points": 85},
{"subject": "Science", "points": 90},
{"subject": "Math", "points": 92},
{"subject": "Science", "points": 88}
]
for item in scores:
subject = item["subject"]
totals.setdefault(subject, 0)
totals[subject] += item["points"]
for subject, total in totals.items():
print(f"{subject}: {total}")
matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
count = 0
for row in matrix:
for val in row:
if val == 1:
count += 1
print(count)
data = {"fruits": ["apple", "banana", "cherry"]}
data["fruits"].append("date")
print(data["fruits"][3])
students = [
{"name": "Priya", "passed": True},
{"name": "Sam", "passed": False},
{"name": "Tess", "passed": True}
]
passed_names = []
for s in students:
if s["passed"]:
passed_names.append(s["name"])
print(passed_names)
grid = [[0] * 3 for _ in range(3)]
grid[1][1] = 9
print(grid)
groups = {
"A": ["Alice", "Ahmed"],
"B": ["Bilal"]
}
groups["A"].append("Anika")
groups["C"] = ["Carlos"]
for group, members in groups.items():
print(f"{group}: {len(members)}")
Each example below contains a bug. Identify the error, explain what is wrong, and write the corrected code.
records = [
{"name": "Mia", "score": 88},
{"name": "Leo", "score": 74}
]
for record in records:
print(records["name"]) # Bug is here
catalog = [
{"title": "Python 101", "tags": ["beginner", "coding"]},
{"title": "Data Basics", "tags": ["data", "intermediate"]}
]
first_tag = catalog["tags"][0] # Bug is here
print(first_tag)
schedule = {
"Monday": ["Math", "Science"],
"Tuesday": ["English", "Art"]
}
schedule["Monday"].append["History"] # Bug is here
table = [[1, 2, 3], [4, 5, 6]]
for row in table:
for value in table: # Bug is here
print(value)
"title", "year", and "rating". Write a loop that prints only the movies with a rating above 8.0.teams where each key is a team name and each value is a list of player names. Add at least two teams with at least three players each. Write a loop that prints each team name followed by how many players it has.row_averages(table) that takes a two-dimensional list of numbers and returns a list of the average of each row. For example, row_averages([[80, 90], [70, 60]]) should return [85.0, 65.0]."name" and "score"), write a function top_scorer(students) that returns the name of the student with the highest score.count_by_grade(students) that takes a list of student dictionaries (each with a "grade" key) and returns a dictionary counting how many students received each grade. For example, if three students got "A" and two got "B", the function returns {"A": 3, "B": 2}.Build a gradebook program that stores student data in a list of dictionaries and supports the following operations through a menu.
Each student record should be a dictionary with:
"name" — a string"scores" — a list of numeric scoresThe program should offer a menu with these options:
Gradebook Menu 1. Add student 2. Add score 3. Show all averages 4. Show top student 5. Quit Choice: 1 Student name: Amara Amara added. Choice: 2 Student name: Amara Score: 88 Score added. Choice: 2 Student name: Amara Score: 95 Score added. Choice: 3 Amara: average = 91.5 Choice: 4 Top student: Amara (91.5)
gradebook = []
def add_student(gradebook, name):
# append a new record to gradebook
pass
def add_score(gradebook, name, score):
# find the student by name and append the score
pass
def student_average(student):
# return the average of student["scores"]
# return 0 if there are no scores
pass
def show_all_averages(gradebook):
# print name and average for each student
pass
def show_top_student(gradebook):
# find and print the student with the highest average
pass
These questions combine Chapter 19 material with earlier topics.
students[0] and students[0]["name"] if students is a list of dictionaries.in.)students = [
{"name": "Ana", "score": 88},
{"name": "Bo", "score": 74},
{"name": "Cara", "score": 95}
]
names = [...] # fill in the comprehension
d = {}
words = ["cat", "dog", "cat", "bird", "dog", "cat"]
for word in words:
d.setdefault(word, 0)
d[word] += 1
print(d)
print(company["departments"][0]["manager"])
setdefault() safely.Chapter 19 gave you the tools to build and navigate nested data. Chapter 20 steps back to ask a harder question: given a problem, which data structure — or combination of structures — should you choose? You have now worked with lists, dictionaries, sets, tuples, nested lists, lists of dictionaries, and dictionaries of lists. Each has strengths and tradeoffs. In Chapter 20, you will learn how to reason about order, uniqueness, lookup speed, mutability, and readability when choosing the right shape for your data. You will also practice designing data structures before writing any code — a habit that pays off in every larger program you build.