Records as Dictionaries
Overview
Name the columns, and the code stops depending on their order.
What You Will Learn in This Lesson
By the end of this lesson, you will be able to:
- Store each row as a dictionary keyed by column name.
- Loop over records and read fields by name.
- Handle a missing field with
.getand a default, on purpose. - Convert a list-of-lists table into a list of records.
Why This Matters
Naming the columns is the single change that makes analysis code survive contact with real files, where columns get added, reordered and occasionally left out. It is also exactly the shape csv.DictReader hands you in the next lesson, and the shape pandas builds a DataFrame from, so this is the representation the rest of the course assumes.
Step 1: A row that says what it holds
Replace the list for each row with a dictionary. The keys are the column names; the values are the cells.
Reading by name
row["score"] says what it reads. It keeps working when someone inserts a column, because there are no positions to shift.
rows = [
{"name": "Ada", "score": 92},
{"name": "Grace", "score": 88},
]
for row in rows:
print(row["name"], row["score"])
Order no longer matters
Two records with their keys written in different orders are the same record. The code that reads them does not care.
a = {"name": "Ada", "score": 92}
b = {"score": 92, "name": "Ada"}
print(a == b)
print(a["score"], b["score"])
Mini Practice #1: Add a field
Try It YourselfAdd a "subject" key to each record, then print each name with its subject. Notice that no existing line had to change.
Step 2: Working with a list of records
Everything from the last lesson still applies — columns are comprehensions, filters are conditions — but now every access has a name.
A column by name
The same comprehension, with a key instead of an index.
rows = [{"name": "Ada", "score": 92}, {"name": "Grace", "score": 88}, {"name": "Alan", "score": 79}]
scores = [r["score"] for r in rows]
print(scores)
print(sum(scores) / len(scores))
Filtering records
A condition on one field selects whole records, so every other field comes along with them.
rows = [{"name": "Ada", "score": 92}, {"name": "Grace", "score": 88}, {"name": "Alan", "score": 79}]
strong = [r for r in rows if r["score"] >= 85]
for r in strong:
print(r["name"])
From lists to records
When data arrives as rows of lists and a header, zip pairs each column name with its value, and dict turns the pairs into a record.
header = ["name", "score"]
table = [["Ada", 92], ["Grace", 88]]
rows = [dict(zip(header, row)) for row in table]
print(rows)
Mini Practice #2: Header plus rows
Try It YourselfTurn table into a list of records with zip, then print the average score using the "score" key.
Step 3: Missing fields, on purpose
Real records have gaps. A dictionary makes you decide what a gap means.
Brackets raise, .get does not
row["score"] on a record without a score raises KeyError. row.get("score") returns None, or the default you give it.
row = {"name": "Alan"}
print(row.get("score"))
print(row.get("score", 0))
Which one you want
A crash is sometimes the right behaviour: a column you expected to always be there is missing, and you want to know loudly. .get is for gaps you know about and have decided how to treat.
rows = [{"name": "Ada", "score": 92}, {"name": "Alan"}]
# Decided: a missing score is left out of the count, not counted as 0.
known = [r["score"] for r in rows if r.get("score") is not None]
print(known)
Key idea: Defaulting a missing score to 0 is a decision with consequences: it drags the average down as if the student had sat the test and scored nothing. Unit 2 spends a lesson on that choice.
Predict, then check: Missing is different from zero
One record has no score; another has a score of zero. Which values should contribute to the average?
records = [{"score": 0}, {"name": "Ada"}, {"score": 80}]
values = [r["score"] for r in records if "score" in r]
print(values)
print(sum(values) / len(values))
Show the output and explanation
[0, 80] 40.0
Membership checks whether a field exists. A truthiness check such as if r.get("score") would discard the real zero and report 80.0. Filling the missing field with zero would instead divide by three. Those are different assumptions, not interchangeable shortcuts.
Try one change: Add another record with score 40. Work out the new denominator before running.
Go deeper: Missing is different from zero
A dictionary lets you refer to a field by name. Direct lookup with record[key] is useful when a required field must exist. get provides a fallback for optional fields, but choosing that fallback is a data decision: a missing measurement does not automatically mean zero.
Trace the example
Predict the output before opening the explanation.
record = {"name": "Ada", "score": 0}
print(record["score"])
print(record.get("age"))
print("score" in record)
Show the output and trace
0 None True
The score key exists and contains a real zero. The age key is absent, so get returns None. Membership tests whether a key exists, independently of whether its value is truthy.
A mistake to watch for
if record.get("score") rejects both a valid zero and a missing value. When zero is valid, use key membership or an explicit is not None check, depending on whether stored None should count as missing too.
Transfer challenge
Create a list with one score of 0, one score of 8, and one record without a score. Collect the available scores without dropping the zero.
Change the example below to solve the challenge. Predict the result, run your code, and compare. This practice is ungraded.
Compare with a worked solution
records = [{"score": 0}, {"score": 8}, {}]
scores = [r["score"] for r in records if "score" in r]
print(scores)
Run both versions. Explain any difference in their output, then change one input and predict the new result.
Step 4: When to Use It
Rows with named fields
Any table you will read by column name, especially one whose columns might change order or grow.
strong = [r["name"] for r in rows if r["score"] >= 85]
Records with optional fields
When some rows legitimately lack a field, .get with a deliberate default says how you treat the gap.
When Not to Use It
Do not use .get with a default to hide a column that should always be there. A missing required field is an error you want to see, so use brackets and let it raise.
End-of-Lesson Exercises
Exercise 1: Everyone at 85 or above
Each row is a dictionary. Print the name of every student whose score is at least 85, one per line.
At least 85 includes 85. Loop the rows, read row["score"], and print row["name"] when the test holds.
Exercise 2: Records from a header
Write to_records(header, table) that turns a header list and a list of row lists into a list of dictionaries. Then write average_of(records, field) returning the mean of that field over the records that have it, or None if none do.
dict(zip(header, row)) builds one record. For the average, collect r[field] for the records where field in r, and return None before dividing if that list is empty.