Errors and exceptions
Read tracebacks, and handle the failures you expect.
Why it matters
Code that talks to the outside world fails in ordinary ways all the time. A file is missing, an API times out, a model returns text that isn't valid JSON. How your code handles those failures is what separates a fragile script from a reliable tool. Reading a traceback quickly is also the fastest debugging skill there is, whether you wrote the code or an AI assistant did.
What you'll be able to do
- Read a traceback from the bottom up and find the line that failed
- Recognize the common exception types and what usually causes each
- Handle an expected failure with try and except, plus else and finally
- Raise your own exception when a function gets input it can't use
What it covers
- Reading a traceback
- The usual suspects
- Handling errors with try and except
- else and finally
- Don't catch everything
- Worked example: messy survey scores
- Keep the try small
Reading a traceback
An error that happens while a program runs is called an exception. If nothing handles it, Python stops and prints a traceback. That's a report of what went wrong, and of the chain of calls that led there. Think of a package's tracking history: the last entry tells you where it got stuck, and the ones above show the route it took. Run this and read the result:
def conversion_rate(conversions, clicks): return conversions / clicks def report(name, conversions, clicks): rate = conversion_rate(conversions, clicks) print(name, rate) report("Spring Sale", 301, 5470) report("Holiday Teaser", 0, 0)
Read a traceback from the bottom up:
- The last line names the exception type and gives a message:
ZeroDivisionError: division by zero. - The line above it shows where it happened: line 2, inside
conversion_rate. - The lines above that show how the program got there: line 9 called
report, which calledconversion_rateon line 5.
Notice the first report printed before the crash. Everything before the failing line ran normally. Python stops at the failure.
On your own computer, tracebacks start with Traceback (most recent call last): and include file names, but they read the same way. The academy shortens them to the lines of your code.
The usual suspects
A handful of exception types cover most of the errors you'll meet:
| Exception | Usually means | Example |
|---|---|---|
NameError | A name is misspelled or wasn't created yet | print(totl) |
TypeError | The operation doesn't work on that type | "5" + 5 |
ValueError | The type is right but the value isn't usable | int("five") |
IndexError | A list position doesn't exist | [1, 2][5] |
KeyError | A dictionary has no such key | {"a": 1}["b"] |
ZeroDivisionError | Dividing by zero | 10 / 0 |
AttributeError | That value has no such method or attribute | (42).upper() |
SyntaxError | Python can't even read the code | a missing colon or bracket |
SyntaxError is different from the rest. Python checks the whole file before running anything, so a syntax error means no line runs. Every other exception happens partway through, when the bad line is reached.
budgets = [5000, 2000, 8000] print(budgets[3])
int("12.5") raise?Show the answer
ValueError
int() accepts a string, so the type is fine, but "12.5" isn't a valid whole number. Wrong value, right type: ValueError. Use int(float("12.5")) if you want 12.
Handling errors with try and except
When you expect a particular failure and know what to do about it, wrap the risky code in try and say what to do in except:
raw_values = ["120", "95", "n/a", "300", ""] total = 0 skipped = 0 for raw in raw_values: try: total += int(raw) except ValueError: skipped += 1 print("Total clicks:", total) print("Skipped:", skipped)
Think of it as a safety net under one particular trick. Python runs the try block. If a ValueError happens anywhere inside it, Python jumps straight to the except block instead of crashing, then carries on after it.
Any other kind of exception still crashes the program. That's what you want: you only handle the failure you planned for.
To see the error itself, give it a name with as:
for raw in ["42", "forty"]: try: print(int(raw)) except ValueError as e: print("Couldn't read", repr(raw), "-", e)
total = 0 for raw in ["5", "x", "7"]: try: total += int(raw) except ValueError: total += 100 print(total)
Show the answer
112
"5" and "7" add 12. "x" can't become a number, so the except block runs and adds 100 instead. The loop carries on, and the total is 112.
On the first call, line 3 succeeds and the except block is skipped. On the second, line 3 fails and execution jumps straight to line 5.
else and finally
Two optional blocks complete the picture. else runs only if the try block raised nothing. finally runs no matter what, even after a return:
def parse_budget(text): try: amount = float(text) except ValueError: print("Not a number:", text) return None else: print("Parsed", amount) return amount finally: print("Done checking", repr(text)) parse_budget("2500") parse_budget("lots")
How often you'll use each:
try/except: constantly.else: now and then. It keeps thetryblock small, so you don't catch errors from code that wasn't risky.finally: for cleanup that must happen, such as closing a connection. For files, Python'swithstatement does the cleanup for you, as you'll see in the files lesson.
Raising your own errors
When a function gets input it can't sensibly use, the clearest thing it can do is stop with a message. That's raise:
def set_budget(amount): if amount <= 0: raise ValueError(f"Budget must be positive, got {amount}") return amount print(set_budget(500)) print(set_budget(-20))
Failing loudly and early beats quietly carrying on with nonsense, like a negative budget that turns into a negative bid three steps later. Code that calls set_budget can still catch the ValueError if it knows how to recover.
Don't catch everything
It's tempting to wrap a big block in except Exception: (or a bare except:) so the program "never crashes". Here's why that backfires:
prices = [28, 64, 18] try: total = sum(prices) average = total / len(prise) # typo: should be prices except Exception: average = 0 print(average)
The typo raised a NameError. The broad except swallowed it, and the program printed a confident, wrong 0. A crash would have shown you the bug in seconds. Silent wrong answers can survive for months.
The rule: catch the specific exceptions you expect and can handle. Let everything else crash where you can see it.
The one common exception is the top of a long batch job, say one that processes 10,000 rows. There you may catch broadly, so one bad row doesn't stop the rest. Even then, record which rows failed and why.
For AI work: when you call an LLM API you'll catch specific failures such as rate limits and timeouts and retry them, while programming mistakes should still crash.
try: ... except Exception: pass "so it never crashes". What's the risk?Show the answer
Bugs like typos are hidden, so the report can show wrong numbers with no warning
A broad except swallows programming mistakes along with the failures you expected. Catch the specific exceptions you can handle, and let the rest show up.
Worked example: messy survey scores
A survey asked for a score from 0 to 10, and people typed whatever they liked. Keep the valid scores, report the rest.
answers = ["8", "10", "seven", "", "9", "11", "6"] def parse_score(text): score = int(text) if score < 0 or score > 10: raise ValueError(f"{score} is outside 0-10") return score scores = [] problems = [] for a in answers: try: scores.append(parse_score(a)) except ValueError as e: problems.append(repr(a) + ": " + str(e)) print("Valid scores:", scores) print("Average:", round(sum(scores) / len(scores), 2)) for p in problems: print("Skipped", p)
It keeps [8, 10, 9, 6], with an average of 8.25, and skips three answers. parse_score can fail in two ways. int() raises a ValueError for text like "seven" or an empty answer. And the function raises its own ValueError for numbers outside the range. Because both use the same exception type, the loop handles them in one place. Nothing is silently dropped: every skipped answer is recorded with the reason. That's exactly what you'd want when the same code runs on 50,000 responses.
Keep the try small
Put only the line that can fail inside try. A long block catches errors you never meant to catch, and hides them.
Practice what you just read
- 8 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
Where should you look first in a traceback?