The Errors Your Data Actually Throws
Three exceptions account for almost every crash involving real data, and each one names the exact thing that was not there. Ask a dictionary for a key it has never heard of, four different ways, and compare what each one hands back.
Worth reading first: When Things Go Wrong on Purpose
Three exceptions account for almost every crash involving real data, and each one names the exact thing that was not there. Ask a dictionary for a key it has never heard of, four different ways, and compare what each one hands back.
Three exceptions cover almost every data crash
You already know how to read a traceback and how to catch what you expect. What is worth having by heart is which three exceptions data actually produces, because recognising the name tells you what went wrong before you read another word.
A dictionary was asked for a key it does not have. The message quotes the key, which is usually the whole diagnosis.
A list or string was asked for a position past its end. Almost always an empty collection you assumed had at least one item.
You went down a level from something with no levels, or did arithmetic on a None. Frequently the delayed consequence of an earlier missing value.
The first two are honest and immediate: they fail at the line that made the wrong assumption. The third is the one that costs an afternoon, because it usually fires a long way from wherever the value actually went missing.
get returns a default instead of raising
.get() asks the same question as square brackets and declines to raise when the answer is no. With one argument it hands back None; with two, whatever you nominated.
scores = {"amara": 18, "ben": 6} scores["dara"] # KeyError: 'dara'scores.get("dara") # Nonescores.get("dara", 0) # 0scores = {"amara": 18, "ben": 6, "chidi": 24}
key = "dara"
scores[key]
Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'dara'
The exception names the missing key. That is more information than any of the quieter options below will give you.
setdefault fills the gap as it reads
.get() reads without writing. .setdefault() reads, and writes the default into the dictionary if the key was missing — which is exactly what building a grouping needs.
by_track = {} for student in students: by_track.setdefault(student["track"], []).append(student["name"]) # {'python': ['Amara', 'Chidi'], 'ml': ['Ben']}The first student on each track finds no list, so setdefault puts an empty one in and returns it. Every later student on that track finds the list already there and appends to it. No conditional, and no key checked twice.
Asking forgiveness instead of permission
There are two ways to write code that might fail, and Python has a stated preference between them. Check first, or attempt and handle the failure.
# Look before you leap: check, then act.if "dara" in scores: total += scores["dara"] # Easier to ask forgiveness: act, then handle the failure.try: total += scores["dara"]except KeyError: passThe second is the more Pythonic of the two, and not merely by convention. The first asks the dictionary the same question twice — once to check, once to fetch — and leaves a gap between the two in which the answer could change. In a program with threads, or where anything else can touch that dictionary, the gap is a real bug and not a theoretical one.
Where checking first genuinely wins is when failure is the common case rather than the exception. Setting up a try is nearly free; raising and catching is not, so a lookup that misses nine times in ten is better off with the in check.
Catching the narrowest exception that fits
A bare except: catches everything, including the typo three lines down and the interrupt you pressed to stop the program. It is the single fastest way to turn a five-second bug into an hour-long one.
# Hides every mistake in the block, including your own typos.try: total += scores[name]except: total += 0 # Names the one failure you actually anticipated.try: total += scores[name]except KeyError: total += 0The second version still crashes if scores turns out to be None, and that is the point — that is a different bug, it deserves a traceback, and the narrow except is what lets it get one.
A None that travels is worse than a crash
.get() is a good tool with one sharp edge: reaching for it reflexively converts a loud, precise failure into a quiet, vague one that surfaces somewhere else entirely.
minutes = record.get("minutes") # missing key → None, no complaintaverage = minutes / chapters # TypeError, forty lines laterThe TypeError names the division, which is the one line in the program that is not the problem. Had the first line raised a KeyError, it would have quoted the missing key and pointed at the record that lacked it.
Key takeaways
- KeyError, IndexError, and TypeError cover almost every data crash, and each names the thing that was missing.
- KeyError and IndexError fail at the line that made the wrong assumption. TypeError usually fires a long way from where the value went missing.
- .get() returns None for a missing key, or a default you supply. It never raises, which is both its use and its risk.
- .setdefault() writes the default in as it reads, which is what makes it the one-line way to build a grouping.
- Attempting and catching beats checking first: the check asks the same question twice and leaves a gap between the answers.
- Check first only when failure is the common case — setting up a try is nearly free, but raising is not.
- A bare except: swallows your own typos and the interrupt key. Name the exception you actually expected.
- A default is right only when it means something. A zero standing in for broken data corrupts every number computed from it, silently.
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.