Exercises — Nested data structures — list of dicts, dict of lists
Before we climb, recall the one law from the parent parent note that everything below rests on:
Throughout, "LoD" = List of dicts, "DoL" = Dict of lists.
The picture below is that transpose law made concrete: the same dataset drawn twice, once as
horizontal row-cards (LoD) and once as vertical column-lists (DoL). Trace the plum-shaded cell — it
is Ravi's score, reached as [1][score] on the left and [score][1] on the right. Look at how
the two coordinates literally swap places as your eye crosses the purple arrow: that swap is the
whole idea, and every exercise on this page is a variation of it.

Our running dataset (memorise the shape, not the numbers):
# List of dicts (LoD) — one dict = one row
people_lod = [
{"name": "Asha", "age": 20, "score": 88},
{"name": "Ravi", "age": 22, "score": 73},
{"name": "Meera", "age": 21, "score": 95},
]
# Dict of lists (DoL) — one list = one column
people_dol = {
"name": ["Asha", "Ravi", "Meera"],
"age": [20, 22, 21],
"score": [88, 73, 95],
}Two Python facts we lean on repeatedly — recap them once here so you never have to leave the page:
Recall Recap: what a
comprehension is (and why we use it)
A list comprehension [f(x) for x in seq] is shorthand for "build a new list by running f
on every x in seq." It replaces the 3-line loop:
out = []
for x in seq:
out.append(f(x))Why we use it here: in a LoD, a column's values are scattered across separate dicts. A
comprehension is the compact way to gather them — [row["score"] for row in people_lod] walks
every row and pulls its score into one flat list. Add if cond at the end to keep only some rows.
Full treatment in List Comprehensions.
Recall Recap:
mutable objects share identity (why one edit can hit two names) Lists and dicts are mutable — you can change their contents in place. Crucially, a variable does not hold a copy of the list; it holds a reference (a pointer) to one list object in memory. If two names point to the same list, editing through one is visible through the other:
a = []
b = a # b points to the SAME list as a
a.append(1)
b # [1] — b changed too, they are one objectWhy it matters here: several exercises accidentally make many dict keys share one list, so appending to one appears to append to all. The fix is always to create a fresh list. Full treatment in Mutable vs Immutable.
Level 1 — Recognition
Goal: tell the two shapes apart and read a single cell without running code.
L1.1 — Which shape is this?
Look at each snippet and say LoD or DoL, and name the outer container type.
A = [{"x": 1}, {"x": 2}]
B = {"x": [1, 2], "y": [3, 4]}Recall Solution L1.1
Ais a LoD: outer is[...](a list), each element is a dict → list of dicts.Bis a DoL: outer is{...}(a dict), each value is a list → dict of lists. The giveaway is always the outermost bracket:[⇒ list ⇒ LoD,{⇒ dict ⇒ DoL.
L1.2 — Read one cell (no code)
Using people_lod above, what does people_lod[1]["name"] evaluate to?
Recall Solution L1.2
Peel outer first. people_lod[1] is the second row (index counting starts at 0):
{"name": "Ravi", "age": 22, "score": 73}. Then ["name"] reads that dict's name field.
Answer: "Ravi".
L1.3 — Same cell, other shape
In people_dol, write the expression for the same cell (row 1, field name).
Recall Solution L1.3
Apply the transpose law — key first, then integer index:
people_dol["name"][1] → "Ravi". Compare to LoD's people_lod[1]["name"]: the
two coordinates literally swapped order.
Level 2 — Application
Goal: run the standard operations — column math, updates, filtering.
L2.1 — Average score, both shapes
Compute the average score over the three people, once from people_lod and once from people_dol.
Recall Solution L2.1
LoD (scores are scattered across dicts → gather first with a comprehension):
total = sum(p["score"] for p in people_lod) # 88 + 73 + 95 = 256
avg = total / len(people_lod) # 256 / 3DoL (column already a flat list → sum directly):
avg = sum(people_dol["score"]) / len(people_dol["score"]) # 256 / 3Both give . This is exactly why DoL wins column math: no gathering step.
See List Comprehensions for the p["score"] for p in ... engine.
L2.2 — Update one field
Raise Ravi's score by 5 in each structure.
Recall Solution L2.2
LoD — you must find the row first (search by name), then edit its field:
for p in people_lod:
if p["name"] == "Ravi":
p["score"] += 5 # 73 → 78DoL — if you already know the position (row index 1), edit directly:
people_dol["score"][1] += 5 # 73 → 78New value in both: 78. (This mutates in place — see Mutable vs Immutable.)
L2.3 — Filter rows
From people_lod, build a new LoD of only people with score >= 85. Give the resulting name list.
Recall Solution L2.3
top = [p for p in people_lod if p["score"] >= 85]
names = [p["name"] for p in top] # ["Asha", "Meera"]Asha (88) and Meera (95) pass; Ravi (73) fails. Result names: ["Asha", "Meera"].
We built a new list rather than deleting in place — safer (see the L5 trap).
Level 3 — Analysis
Goal: reason about which shape a task prefers, and predict errors.
L3.1 — Predict the error
What happens if you run people_lod["name"] (note: LoD, not DoL)?
Recall Solution L3.1
people_lod is a list. A list can only be indexed by an integer (or a slice), never by a
string. Python raises TypeError: "list indices must be integers or slices, not str."
The fix is the transpose law: integer first → people_lod[0]["name"].
L3.2 — Which shape, and why?
For each task, name the better structure and give the one-line reason.
- Append a brand-new person record.
- Find the maximum
ageacross everyone. - Store 1 million rows that all share the same 3 keys, using least memory.
Recall Solution L3.2
- LoD — a record is a self-contained dict you just
.append(...)to the list. - DoL — the
agecolumn is already a flat list, somax(people_dol["age"])works directly. (Heremax = 22.) - DoL — the key strings (
"name","age","score") are stored once as dict keys, not repeated inside a million dicts. LoD would store the key strings a million times each. This is the shape Pandas DataFrame uses under the hood.
L3.3 — Count of matching rows
Using people_lod, how many people are strictly older than 20? Show the expression.
Recall Solution L3.3
count = sum(1 for p in people_lod if p["age"] > 20) # Ravi(22), Meera(21) → 2sum(1 for ...) counts the rows that pass. Answer: 2. (Asha is exactly 20, so > 20 excludes her.)
Level 4 — Synthesis
Goal: build one shape from the other, and construct new structures.
L4.1 — LoD → DoL (the transpose)
Convert this LoD to a DoL by hand, then in one line.
lod = [{"name": "Asha", "score": 88},
{"name": "Ravi", "score": 73}]Recall Solution L4.1
By hand: walk each key, collect that key's value from every row in order.
namecolumn:["Asha", "Ravi"]scorecolumn:[88, 73]One-liner:
dol = {k: [row[k] for row in lod] for k in lod[0]}
# {"name": ["Asha", "Ravi"], "score": [88, 73]}Outer comprehension iterates the keys (taken from the first row lod[0], since all rows share
keys). Inner comprehension walks every row to gather that key's values. This literally performs the
swap.
L4.2 — DoL → LoD (the reverse transpose)
Convert back:
dol = {"name": ["Asha", "Ravi"], "score": [88, 73]}Recall Solution L4.2
keys = list(dol) # ["name", "score"]
n = len(dol[keys[0]]) # rows = length of any column = 2
lod = [{k: dol[k][i] for k in keys} for i in range(n)]
# [{"name":"Asha","score":88}, {"name":"Ravi","score":73}]Why list(dol) gives ["name", "score"]: in Python, iterating a dict — or wrapping it in
list(...) — walks its keys, not its values. So list(dol) is a plain list of the column
names, in insertion order. (To get values you would ask for dol.values(); to get pairs,
dol.items().) We want the column names here, so list(dol) is exactly right.
Then loop the row index i from 0 to n-1. For each i, build one dict by reading the i-th
element of every column. That's dol[k][i] — the DoL coordinate — assembled into lod[i][k].
L4.3 — Group into a dict of lists
Given a LoD of transactions, build a DoL mapping each city to the list of amounts in that city.
tx = [{"city": "Pune", "amt": 100},
{"city": "Delhi", "amt": 40},
{"city": "Pune", "amt": 60}]Recall Solution L4.3
groups = {}
for t in tx:
groups.setdefault(t["city"], []).append(t["amt"])
# {"Pune": [100, 60], "Delhi": [40]}setdefault(key, []) returns the existing list for key, or creates a fresh empty list the
first time we see that key, then we .append. Result: Pune → [100, 60], Delhi → [40].
Sum per city: Pune = 160, Delhi = 40.
Level 5 — Mastery
Goal: survive the corner cases — missing keys, mutation-while-iterating, degenerate inputs.
L5.1 — Ragged rows (missing keys)
This LoD has a row missing the score key. Write a transpose to DoL that does not crash, filling
missing cells with None.
lod = [{"name": "Asha", "score": 88},
{"name": "Ravi"}] # no "score"!Recall Solution L5.1
First collect the full key set across all rows (not just lod[0], which would miss keys some
rows have and others don't). Then use .get(k) which returns None when a key is absent.
keys = {k for row in lod for k in row} # {"name", "score"}
dol = {k: [row.get(k) for row in lod] for k in keys}
# {"name": ["Asha", "Ravi"], "score": [88, None]}The score column is [88, None] — Ravi's missing cell became None instead of raising
KeyError. This is exactly the shape JSON and APIs hands you when records are inconsistent.
L5.2 — Delete rows safely
Remove everyone with score < 80 from people_lod. Explain why the naive in-place loop is wrong,
then give the correct code and the surviving names.
Recall Solution L5.2
Wrong (mutating while iterating):
for p in people_lod:
if p["score"] < 80:
people_lod.remove(p) # BUG: shifts indices mid-loop → skips elementsWhen you remove an item, everything after it shifts left, but the loop's internal counter still advances — so the element that slid into the freed slot is skipped. Correct (build a new list):
people_lod = [p for p in people_lod if p["score"] >= 80]
# keeps Asha(88), Meera(95); drops Ravi(73)Surviving names: ["Asha", "Meera"]. See Loops and Iteration for why filtering into a new
list is the safe idiom.
L5.3 — Degenerate inputs
Predict the result (or error) for each, and give the safe version.
- Average score of an empty LoD
[]. - Transpose an empty LoD
[]to DoL.
Recall Solution L5.3
sum(p["score"] for p in []) / len([])→ sum is0,lenis0→ZeroDivisionError. Safe:avg = total / n if n else 0(orNone). Guard the divide-by-zero explicitly.{k: [...] for k in lod[0]}→lod[0]on an empty list isIndexError(no first row). Safe:dol = {} if not lod else {k: [row.get(k) for row in lod] for k in lod[0]}. An empty dataset has no columns, so an empty DoL{}is the right answer.
Score yourself
Connections
- Lists — the outer container of LoD, inner of DoL
- Dictionaries — key→value lookups powering both shapes
- List Comprehensions — the engine behind every transpose here
- JSON and APIs — where ragged real-world LoDs come from
- Pandas DataFrame — a fast dict-of-lists with column ops built in
- Loops and Iteration — safe traversal and the mutate-while-iterate trap
- Mutable vs Immutable — why the shared-list bug bites