Chapter 54: Error Handling and Exceptions
James loads a JSON file full of SmartNotes data. The first ten notes parse perfectly: title, body, word count, tags. Then the program crashes. He stares at the traceback. The eleventh note has "word_count": "many" instead of a number. One broken value in one record, and the entire program stops dead.
Emma looks at the screen. "What happens when data arrives broken?"
James thinks about it. Every function he has written so far assumes the input is valid. categorize_note assumes word_count is an integer. format_note_summary assumes title is a non-empty string. When those assumptions hold, everything works. When they break, Python raises an exception and the program dies.
"You cannot control what data comes in," Emma says. "You can only control what your program does when the data is wrong. That is what this chapter teaches."
Why Error Handling Matters for Specification
In Chapter 49, you wrote function contracts: name, parameters, return type, docstring. Those contracts describe what a function does when everything goes right. Error handling describes what a function does when something goes wrong. A robust load_notes_from_json does not just return a list of notes; it also explains what happens when the file is missing, the JSON is malformed, or a required field contains the wrong type.
Testing becomes richer too. In Chapter 52, you used pytest.raises to verify that functions raise the right exceptions. Now you will write the code that raises those exceptions and the code that catches them. Every raise you write gets a pytest.raises test. Every except you write gets a test that triggers the error path.
What You Will Learn
By the end of this chapter, you will be able to:
- Catch exceptions with
try/except/elseand inspect error messages withas e - Raise your own exceptions with meaningful, f-string-formatted messages
- Navigate the built-in exception hierarchy and choose the right exception type
- Use
finallyfor guaranteed cleanup andwithfor safe resource handling - Write manual validation functions that check every field of a data structure
Chapter Lessons
| Lesson | Title | What You Do | Duration |
|---|---|---|---|
| 1 | Catching Errors with try/except | Write try/except blocks, use as e, add else clauses, avoid bare except | 25 min |
| 2 | Raising Your Own Exceptions | Raise ValueError and TypeError with meaningful messages, test every raise | 20 min |
| 3 | The Exception Hierarchy and Best Practices | Navigate built-in exceptions, use decision tables, understand finally | 20 min |
| 4 | Resource Safety: finally and with | Open files with with, load JSON, handle FileNotFoundError and JSONDecodeError | 20 min |
| 5 | Manual Validation Pain | Write a 30+ line validation function, feel the repetition, preview Pydantic | 20 min |
| 6 | Chapter 54 Quiz | 50 scenario-based questions covering all error handling concepts | 25 min |
PRIMM-AI+ in This Chapter
Every lesson includes a PRIMM-AI+ Practice section following the five-stage cycle from Chapter 42. This is Phase 3: you are now WRITING error handling code, building on the testing patterns (Chapter 52) and function contracts (Chapter 49) you already own.
| Stage | What You Do | What It Builds |
|---|---|---|
| Predict [AI-FREE] | Predict which except block catches an error, or whether a raise triggers, with a confidence score (1-5) | Calibrates your exception-flow intuition |
| Run | Execute the code or run pytest, compare to your prediction | Creates the feedback loop |
| Investigate | Write a trace artifact explaining how Python matched the exception to the handler | Makes your error-handling reasoning visible |
| Modify | Change the exception type or the input and predict the new behavior | Tests whether your understanding transfers |
| Make [Mastery Gate] | Write an error-handling function from scratch with tests for every path | Proves you can handle errors independently |
Syntax Card: Chapter 54
Reference this card while working through the lessons. Every construct shown here appears in at least one lesson.
# -- try/except/else/finally ---------------------------------
try:
value: int = int(user_input)
except ValueError as e:
print(f"Bad input: {e}")
else:
print(f"Parsed: {value}") # Runs only if no exception
finally:
print("Always runs") # Cleanup, always executes
# -- raise ---------------------------------------------------
raise ValueError("word_count must be an integer")
raise TypeError(f"Expected str, got {type(value).__name__}")
# -- Exception decision table --------------------------------
# ValueError : right type, wrong value (int("hello"))
# TypeError : wrong type entirely (len(42))
# KeyError : missing dictionary key (d["missing"])
# FileNotFoundError : file does not exist (open("nope.txt"))
# -- with open (context manager) -----------------------------
with open("notes.json") as f:
data = json.load(f)
# File is automatically closed, even if json.load crashes
# -- json.load -----------------------------------------------
import json
with open("notes.json") as f:
notes: list[dict[str, str]] = json.load(f)
Prerequisites
Before starting this chapter, you should be able to:
- Write
if/elif/elsebranches andforloops (Chapter 50) - Define and use
@dataclasstypes with typed fields (Chapter 51) - Write tests with
assert,pytest.raises, andpytest.approx(Chapter 52) - Iterate on AI-generated code using review-test-refine cycles (Chapter 53)
- Read
raisestatements in AI-generated code (Chapter 52 Lesson 4 preview)
The SmartNotes Connection
At the end of this chapter, you will tackle the pain of manual validation using SmartNotes data. You will write two functions that bring together everything from Lessons 1 through 4:
load_notes_from_json(path: str) -> list[dict[str, object]]: Opens a JSON file withwith open, catchesFileNotFoundErrorandjson.JSONDecodeError, and returns the parsed data or raises a clear error.validate_note_data(data: dict[str, object]) -> Note: Checks six fields (title, body, word_count, author, tags, is_draft) withisinstanceand manual bounds checking, raisingTypeErrororValueErrorfor every invalid field.
The validation function will be painfully long. You will check isinstance for every field, validate string lengths, confirm non-negative integers, and verify list contents. Thirty lines of repetitive checking for just six fields. That pain is intentional. In Chapter 55, Pydantic replaces those thirty lines with six field declarations. But you need to feel the problem before the solution makes sense.