Unit 1 • Lesson 3

Records as Dictionaries

Overview

Name the columns, and the code stops depending on their order.

Beginner 20–25 min

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 .get and 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.

1

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"])
2

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 Yourself

Add a "subject" key to each record, then print each name with its subject. Notice that no existing line had to change.

Press Run to see output

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.

1

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))
2

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"])
3

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 Yourself

Turn table into a list of records with zip, then print the average score using the "score" key.

Press Run to see output

Step 3: Missing fields, on purpose

Real records have gaps. A dictionary makes you decide what a gap means.

1

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))
2

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.

Press Run to see output
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.

Press Run to try it, then Check my work to mark it.

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.

Press Run to try it, then Check my work to mark it.