1.2.26 · D5Introduction to Programming (Python)

Question bank — Nested data structures — list of dicts, dict of lists

2,062 words9 min readBack to topic

Before you start, build the picture right here so nothing is deferred. Both structures hold the same grid of cells. The only difference is which axis you slice first:

Figure — Nested data structures — list of dicts, dict of lists

Look at the figure: on the left, a List of Dicts (LoD) is drawn as a stack of row cards — each card is one dict {name, age, marks}. On the right, a Dict of Lists (DoL) is drawn as side-by-side column strips — each strip is one list. Follow the coloured arrows: the single cell "Ravi's marks = 73" lives at students[1]["marks"] in the LoD and at students["marks"][1] in the DoL. The two index paths cross over — that crossing is the transpose.


True or false — justify

Every item is a statement. Decide true/false, then give the reason.

LoD and DoL can store exactly the same information.
True. They are transposes — same cells, just organised rows-first vs columns-first. No data is lost either way.
students[0]["name"] and students["name"][0] always return the same value regardless of structure.
False. Only one form is valid per structure: [i][c] for LoD, [c][i] for DoL. The other form raises an error, so they are not interchangeable.
In a LoD, the outer container being a list means you must use an integer to index it first.
True. A list only accepts integer (or slice) indices; the string key is used on the inner dict, one level deeper.
In a DoL, every column-list must have the same length.
True (for well-formed data). Row exists only if every column has an element at index ; unequal lengths mean some "rows" are missing fields and the structure is broken.
After dol = lod2dol(lod) then back = dol2lod(dol), the check back == lod is True but back is lod is False (all rows sharing keys).
True. == compares contents (equal cells → equal), so it passes; is compares identity (same object in memory), and back is a freshly built list, so is fails. Round-tripping preserves value, not identity.
Round-tripping LoD→DoL→LoD preserves the data even when some rows lack keys.
False. With ragged rows the LoD→DoL step must fill missing cells with a default (e.g. None), so the rebuilt LoD gains keys the original lacked — == now fails too, not just is.
A DoL uses less memory than the equivalent LoD when there are many rows.
True (typically), and here is why: with 3 fields and rows, a LoD stores the field-name strings times (once per row-dict); a DoL stores them just times as dict keys. For that is ~3000 vs ~3 key-string references — the values themselves cost the same in both.
You can append a new record to a DoL as easily as to a LoD.
False. LoD appends one self-contained dict in a single step. DoL requires appending to every column-list separately and keeping them aligned.
for s in students: iterates rows in a LoD and columns in a DoL.
True. Iterating a list yields its elements (row-dicts); iterating a dict yields its keys (column names) — a classic surprise if you forget the outer container changed.
In modern Python (3.7+) the keys of a DoL come out in a random order when you iterate.
False. Dicts preserve insertion order, so iterating yields keys in the order you defined the columns — reliable and repeatable, not arbitrary.

Spot the error

Each line describes code. Say what breaks and why.

students["name"] where students is a list of dicts.
A list cannot be indexed by a string, so this raises TypeError. You mixed up DoL access on a LoD — use students[0]["name"].
students[0] where students is a dict of lists with string keys.
KeyError: 0 — the dict has no key 0; its keys are field names like "name". Use students["name"] first.
dol = dict.fromkeys(["name","age"], []) then dol["name"].append("Asha") — trace the object IDs.
id(dol["name"]) == id(dol["age"]) is True: both keys point at the one list built by fromkeys, so after the append both read ["Asha"]. Minimal proof:
>>> dol = dict.fromkeys(["name","age"], [])
>>> dol["name"].append("Asha")
>>> dol{'name': ['Asha'], 'age': ['Asha']}. Fix with {c: [] for c in cols}, where each [] is a fresh object with its own id — see Mutable vs Immutable.
{k: [row[k] for row in lod] for k in lod[0]} when one row is {"name":"Ravi"} (no "marks").
KeyError: "marks" on that row. The comprehension trusts every row to share lod[0]'s keys. Use row.get(k) for a None default.
for s in students: students.remove(s) to clear a LoD.
Removing while iterating shifts indices, so the loop skips elements — you never fully clear it. Build a new list instead: students = [s for s in students if s["marks"] >= 40], keeping only rows whose condition holds (here, pass marks) rather than deleting in place.
avg = sum(students["marks"]) on a list of dicts.
students["marks"] already fails with TypeError (list indexed by string). Column math only works after building the column, or in DoL directly.
n = len(students) used as the row count of a dict of lists.
len(students) on a DoL counts columns (number of keys), not rows. Row count is len(students[any_key]) — the length of one column.
row["age"] += 5 inside for row in students where students is a DoL.
row is a string key like "age", not a dict, so row["age"] raises TypeError. Iterating a dict gives keys — to touch a cell use students["age"][i].

Why questions

Answer the reasoning, not just a fact.

Why does DoL win for computing a column average?
The column is already one flat list, so sum(col)/len(col) runs directly — no gathering across records with a comprehension first.
Why does LoD win for adding or deleting a whole record?
Each record is a single self-contained dict, so append (add) or list-filtering (remove) touches exactly one object, not every column.
Why do the indices swap order between the two forms ([i][c] vs [c][i])?
Because the structures are transposes: whichever container is outer is accessed first. LoD asks row-then-field; DoL asks field-then-row.
Why is LoD the natural match for JSON from an API?
JSON typically sends an array of objects — a list where each object is a record with named fields, which maps one-to-one onto a Python list of dicts.
Why can iterating a DoL with for x in students bite beginners?
Iterating a dict yields its keys (column names), not rows. Coming from LoD (which yields rows) the same syntax silently means something different.
Why does the shared-list bug only appear with dict.fromkeys(cols, []) and not with a dict comprehension?
fromkeys evaluates the [] once and points every key at that one object; the comprehension {c: [] for c in cols} runs [] fresh on each iteration, giving independent lists.
Why must LoD→DoL conversion collect keys carefully when rows may differ?
If you trust only lod[0]'s keys you miss fields other rows have; the safe key set is {k for row in lod for k in row} over all rows.
Why is a Pandas DataFrame often described as a "turbocharged DoL"?
A DataFrame stores each column as one contiguous array (column-first, like DoL), so column operations are fast — but it also lets you slice rows conveniently, combining both strengths.

Edge cases

Boundary and degenerate inputs — the scenarios people forget.

An empty LoD []: what does lod[0] do, and how do you get its keys?
lod[0] raises IndexError (no row 0) and there are no keys to read — an empty LoD has no schema. Guard with if lod: before touching lod[0].
An empty DoL {}: how many rows does it have?
Undefined — with no columns there is no column to measure. len(dol) is 0 (zero columns), and row count is unknowable until at least one key exists.
students[-1] on a LoD vs students[-1] on a DoL — what does each mean?
On a LoD the outer container is a list, so -1 is the last row (valid, students[len-1]). On a DoL the outer container is a dict, so -1 is treated as a key; unless a key literally -1 exists it raises KeyError. Negative indexing only works on the list layer.
students[1:3] (a slice) on a LoD vs on a DoL — which works?
On a LoD it works: slicing a list returns rows 1 and 2 as a smaller LoD. On a DoL it raises TypeError — dicts are not sliceable by start:stop. To slice rows in a DoL you must slice each column: {c: students[c][1:3] for c in students}.
A DoL where columns have different lengths ("name" len 3, "age" len 2): is row 2 valid?
Row 2 exists only for "name"; asking dol["age"][2] raises IndexError. The structure is malformed — every column must share the row count.
A LoD where row 0 has extra keys the others lack: does {k: [row[k] ...] for k in lod[0]} work?
No — it iterates row 0's keys, so it tries to read a key later rows don't have, raising KeyError. Use .get(k) or a full key set.
A LoD with a single record [{"a":1}]: is it still a valid LoD, and its DoL?
Yes. One row is fine; its DoL is {"a":[1]} — every column becomes a length-1 list. The transpose relationship still holds.
A DoL with a single column {"a":[1,2,3]}: what is its LoD form?
[{"a":1},{"a":2},{"a":3}] — three rows, each a one-field dict. Row count came from the single column's length.
Duplicate field values across rows (two students named "Asha") — does either structure break?
Neither breaks; both allow duplicate values. Only dict keys must be unique, and keys are field names, not values.
A field value that is itself a list (e.g. "marks":[88,73] per student in a LoD): does access change?
No — students[0]["marks"] still returns that inner list; you just index one level deeper (students[0]["marks"][1]) to reach an element. Nesting composes cleanly.

Connections

  • Lists — the integer-indexed container behind LoD's outer and DoL's inner
  • Dictionaries — key→value lookup powering both
  • List Comprehensions — the transpose engine and the ragged-key traps
  • JSON and APIs — why list-of-dicts is the common wire format
  • Pandas DataFrame — column-first storage, the DoL taken to production
  • Loops and Iteration — why iterating a dict vs a list surprises you
  • Mutable vs Immutable — the root of the shared-inner-list bug