A Row Is Just a Dictionary
A spreadsheet has rows and columns; Python has neither, and still holds the same data more honestly. Build a table out of nothing but a list and some dictionaries, then filter it down to three rows without ever writing a counter.
Worth reading first: Looking Things Up Instead of Counting Along
A spreadsheet has rows and columns; Python has neither, and still holds the same data more honestly. Build a table out of nothing but a list and some dictionaries, then filter it down to three rows without ever writing a counter.
One record is a dictionary with agreed keys
A record is one thing you know several facts about: a student, an order, a sensor reading. In Python it is almost always a dictionary, because a dictionary lets you name the facts instead of remembering their positions.
student = {"name": "Amara", "track": "python", "chapters": 18} # The same thing as a tuple. Technically fine, practically hostile:student = ("Amara", "python", 18)Both hold three values. Only one of them still makes sense in six months, when student[2] could plausibly be chapters, minutes, or the year they joined. The dictionary version costs a few more characters and buys you the ability to read your own code.
A list of those records is a table
Put those records in a list and you have a table: the list gives you rows in order, each dictionary gives you named columns. There is no table type involved, and none is needed.
students = [ {"name": "Amara", "track": "python", "chapters": 18, "minutes": 164}, {"name": "Ben", "track": "ml", "chapters": 6, "minutes": 71}, {"name": "Chidi", "track": "python", "chapters": 24, "minutes": 231},]This shape is worth recognising on sight, because it is what almost every API hands back and what almost every CSV reader produces. Learn to work with it directly and a large amount of real data work stops needing a library at all.
>>> students
| name | track | chapters | minutes |
|---|---|---|---|
| Amara | python | 18 | 164 |
| Ben | ml | 6 | 71 |
| Chidi | python | 24 | 231 |
| Dara | vibecoding | 9 | 88 |
| Esi | python | 11 | 102 |
| Fen | ml | 21 | 195 |
Six dictionaries in a list. Every one carries the same four keys, and that agreement is the only thing making this a table rather than six unrelated objects.
Filtering rows without counting them
The instinct from other languages is to walk the list by index and collect what matches. Python asks the question of each row instead, and the comprehension you already know is the whole mechanism.
finished = [s for s in students if s["chapters"] >= 18] # Two conditions read exactly as they sound:python_finishers = [ s for s in students if s["track"] == "python" and s["chapters"] >= 18]No index, no counter, and no chance of stopping one row early. What comes back is a new list holding the same dictionaries — not copies of them. Change a field on a filtered row and the original table sees it, which is usually what you want and occasionally a surprise.
Sorting a table by any column you like
A key function receives one whole row and returns the single value to order by. The rows are never taken apart, which is why the same one-liner works no matter how many columns a record has.
by_minutes = sorted(students, key=lambda s: s["minutes"], reverse=True) # Two columns at once: track first, then most chapters within each track.ranked = sorted(students, key=lambda s: (s["track"], -s["chapters"]))Returning a tuple from the key sorts by the first element, then breaks ties with the second. The minus sign flips just that one field, which is the trick that lets you sort one column ascending and another descending in a single pass.
Summarising a column down to one number
Pulling one column out and reducing it is two steps written as one line, and the generator expression means the intermediate list is never built.
total = sum(s["minutes"] for s in students)average = total / len(students)longest = max(students, key=lambda s: s["minutes"]) print(longest["name"])# ChidiWhere a list of dicts stops being enough
This shape is excellent until one of three things becomes true, and it is worth knowing the boundary before you hit it at speed.
Still the right tool
Thousands of rows, read once or twice, filtered and summarised. Plain Python handles this comfortably and adds no dependency to your project.
Time to reach further
Millions of rows, repeated lookups by the same field, or genuine column-at-a-time maths. That is what a dictionary keyed by id, or a real dataframe library, exists for.
The middle case is the interesting one: if you keep scanning the whole list to find one student by name, you do not need a bigger library — you need a dictionary keyed by name instead, which the last chapter of this part is about.
Key takeaways
- A record is a dictionary with named fields. A list of records is a table, and no table type is required to make one.
- This list-of-dicts shape is what most APIs and CSV readers hand back, so recognising it saves reaching for a library.
- Filtering is a comprehension asking a question of each row — no index and no counter to get wrong.
- A sort key receives the whole row and returns one value. Return a tuple to sort by several columns, and negate a number to flip just that one.
- Nothing makes every record carry the same keys. That agreement is yours to keep, and breaking it is a common source of KeyError.
- A filtered list holds the same dictionaries, not copies — editing a row after filtering edits the original table.
- max() and min() raise on an empty list rather than guess. Pass a default when empty is a normal case.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.