Structuring Your Data

Nested Structures: Lists of Dicts and Dicts of Lists


Learning Objectives

  • You can build and read a list of dictionaries, where each dictionary is one record.
  • You can build and read a dictionary whose values are lists, grouping items under each key.
  • You can choose between these two shapes based on how you need to access the data.
  • You can read nested access expressions like data["math"][0]["minutes"] without losing track.

Lists and dictionaries each hold a collection of values. Their real power shows up when you nest them — a list whose items are dictionaries, or a dictionary whose values are lists. These two shapes cover most of the structured data you will meet.

This chapter looks at both, and at how to decide which one fits a given problem.

A List of Dictionaries

A list of dictionaries is a list where every item is a dictionary with the same keys. It is the natural way to hold a collection of records.

Here is the study log’s session data in this shape:

sessions = [
{"subject": "math", "minutes": 45, "date": "2024-03-10"},
{"subject": "physics", "minutes": 90, "date": "2024-03-10"},
{"subject": "history", "minutes": 30, "date": "2024-03-11"},
]

Compare this to the list-of-rows the project uses now:

sessions = [
["math", 45, "2024-03-10"],
["physics", 90, "2024-03-10"],
["history", 30, "2024-03-11"],
]

Same information, same number of sessions. The difference is that each record now labels its own fields. To get a value out, you index twice — once into the list for the record, once into the dictionary for the field:

sessions[0] # {'subject': 'math', 'minutes': 45, 'date': '2024-03-10'}
sessions[0]["subject"] # 'math'
sessions[1]["minutes"] # 90

You select the first record with the index sessions[0]; ["subject"] selects a field within it. Read the expression left to right, one bracket at a time, and it stays clear.

Looping over a list of dictionaries gives you one dictionary per step:

for session in sessions:
print(f"{session['subject']}: {session['minutes']} min")

Notice there is no unpacking in the loop header. With list-of-rows you wrote for subject, minutes, day in sessions to give the positions names. With list-of-dicts the fields are already named, so you take the whole dictionary as session and reach into it by key.

Inside an f-string, the key needs quotes that differ from the f-string’s own, which is why this uses session['subject'] with single quotes inside the double-quoted string.

Pages by author

0 / 10 points

Write a function named pages_by_author(books).

Each item in books is a dictionary with this shape:

{"title": title_string, "author": author_string, "pages": pages_integer}

The function must return a dictionary that maps each author to the total number of pages written by that author.

Requirements:

  • return {} for an empty input list,
  • add together the "pages" values for books by the same author,
  • do not change the input list or its dictionaries,
  • do not print anything,
  • and do not use input(...).

For example:

books = [
{"title": "North Road", "author": "M. Chen", "pages": 180},
{"title": "Harbor Light", "author": "A. Vale", "pages": 240},
{"title": "Second Harbor", "author": "A. Vale", "pages": 210},
]

pages_by_author(books) should return:

{"M. Chen": 180, "A. Vale": 450}

A Dictionary of Lists

A dictionary of lists turns the nesting inside out: it is a dictionary where every value is a list. This is the natural way to hold items grouped under a key.

Suppose you want, for each subject, the list of every session length recorded for it:

minutes_by_subject = {
"math": [45, 60, 25],
"physics": [90],
"history": [30],
}

Now minutes_by_subject["math"] is the whole list [45, 60, 25], and you can do anything to it that you would do to any list:

math_minutes = minutes_by_subject["math"]
print(sum(math_minutes)) # 130
print(max(math_minutes)) # 60
print(len(math_minutes)) # 3

You build this shape with the same accumulation pattern from the previous chapter, except the running value is a list you append to rather than a number you add to:

def minutes_by_subject(sessions):
grouped = {}
for subject, minutes, day in sessions:
if subject not in grouped:
grouped[subject] = []
grouped[subject].append(minutes)
return grouped

The if subject not in grouped line sets up an empty list the first time each subject appears, so the .append on the next line always has a list to append to. This is the list-valued version of the counts.get(subject, 0) trick: you cannot use .get quite as neatly here, because you want to append to the same list rather than replace it, so the explicit “create it if missing” check is clearer.

This grouped shape is often a useful intermediate step. Once you have minutes_by_subject, computing per-subject totals, averages, maxima, or counts is just a matter of applying sum, len, or max to each value list.

Choosing Between the Two

The two shapes hold related information but answer different questions.

A list of dictionaries keeps each record whole and separate. It is the right shape when you care about individual records and want to walk through them: every session, in order, each with all its fields. The study log’s sessions are fundamentally a sequence of records, so this is the shape the project will use to hold them.

A dictionary of lists groups records by some key, discarding everything except the grouped-on value. It is the right shape when you care about the groups: all the math minutes together, all the physics minutes together. It is what you build from the records when you want to summarise by category.

A useful way to decide: ask how you will look the data up. If you will reach for things by position in a sequence or walk all of them, you want a list — of dictionaries, if each item has named fields. If you will reach for things by name or category, you want a dictionary — of lists, if each name maps to several items.

The study log will hold its sessions as a list of dictionaries, because sessions are a sequence of records. When it summarises by subject, it will build dictionaries from them — exactly as the previous chapter did.

Choosing a grouped shape

0 / 5 points

You have many inspection records, and you often need all scores for one building at once.

Which shape best matches that access pattern?

Reading Deeper Nesting

You can nest further — a dictionary of lists of dictionaries, say — and the access expressions get correspondingly longer. The summaries from the previous chapter produced one such shape:

summaries = {
"math": {"count": 3, "total": 130, "average": 43.33},
"physics": {"count": 1, "total": 90, "average": 90.0},
}

This is a dictionary whose values are dictionaries. To reach the inner values you index twice by key:

summaries["math"] # {'count': 3, 'total': 130, 'average': 43.33}
summaries["math"]["total"] # 130

The rule for reading any nested access is the same: go left to right, one bracket at a time, and say out loud what each step selects. summaries["math"]["total"] is “in summaries, the entry for math; in that, the total”.

If an expression has so many brackets that you lose the thread reading it that way — data["math"][0]["sessions"][2]["minutes"] — that is usually a sign the shape is too deeply nested, and a flatter design or a helper variable would serve you better.

Reading nested access

0 / 5 points

Given library["fiction"][1]["pages"], what does [1] select?

Check Your Understanding

  1. In the expression sessions[1]["minutes"], what does each of the two bracket expressions select?
  2. When you loop over a list of dictionaries, why is there no unpacking in the loop header, unlike the for subject, minutes, day in sessions loops from the previous part?
  3. What is the difference between a list of dictionaries and a dictionary of lists, and when would you reach for each?
  4. Why does the dictionary-of-lists builder need the if subject not in grouped check before it appends?
  5. The chapter says an access expression like data["math"][0]["sessions"][2]["minutes"] is “usually a sign” of something. A sign of what?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0