Module 6 ended with an uncomfortable list of "band-aids": Path.exists() before reading, "don't botch the JSON when you edit it", "pray the CSV brings numbers". Papyrus already has memory — the catalog, sales, members and backups live in data/ — but that memory is fragile: if catalog.json isn't where it should be, if a CSV row brings "free" in the price column, or if Julia types an impossible number, the program dies with a block of red text splashed across the screen. That block of text has a name (a traceback), that "dying" has a mechanism (an exception that nobody catches), and this whole module is about mastering both. In this first lesson we won't fix anything yet: we'll understand what an exception really is, learn to read a traceback the way you'd read an incident report, meet the built-in exceptions Papyrus has already suffered without knowing their names, and adopt the philosophy Python prefers: asking forgiveness rather than permission.
Contents
- What an exception is: the mechanism, not the disaster
- Anatomy of a traceback: read it bottom-up
- Syntax errors vs runtime exceptions
- The built-in exceptions Papyrus already knows (without knowing it)
- The exception hierarchy:
Exceptionas the mother class - EAFP vs LBYL: why
Path.exists()was a band-aid
What an exception is: the mechanism, not the disaster
An exception is an object that Python creates when an operation cannot complete, and that interrupts the normal flow of the program to travel upwards looking for someone to take charge. Three key ideas:
- It's an object. Just as
Book("Hamlet", 9.95, 6)is an instance of theBookclass, aValueError("price cannot be negative")is an instance of theValueErrorclass. It carries a message inside and, as we'll see in 07-04, it can carry attributes of its own. - It propagates. When it occurs, execution abandons the current line, leaves the function, leaves the function that called it, and so on. This upward journey is called propagation, and it's what makes an error on line 3 of a helper function end up looking like a failure of the whole program.
- If nobody catches it, the program terminates. Python prints the traceback and hands control back to the operating system. That's what Ana has been seeing so far: unhandled exceptions.
Note the nuance: the exception is not the problem. The exception is the structured alert that a problem occurred. The disaster is having nobody listening for the alert.
# book_record.py — the same innocent code from module 1
price_text = input("Book price: ") # Julia types: free
price = float(price_text) # the exception is born here
print(f"With VAT: {price * 1.04:.2f} EUR") # this line never runsWhen float("free") can't produce a number, Python builds a ValueError object, abandons line 3 and, since nobody catches it, the program terminates. The print line never gets executed: propagation cuts the flow dead.
Anatomy of a traceback: read it bottom-up
This is what Python prints as the previous example dies:
Traceback (most recent call last):
File "book_record.py", line 2, in <module>
price = float(price_text)
ValueError: could not convert string to float: 'free'And here is a real case with several chained functions, in the style of module 6's register.py:
Traceback (most recent call last):
File "register.py", line 42, in <module>
catalog = load_catalog()
File "register.py", line 18, in load_catalog
with open(BASE / "data" / "catalog.json", encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'data/catalog.json'The golden rule for reading it: start at the bottom.
- Last line: the exception type (
FileNotFoundError) and its message. It's the what. Always read it first. - Second-to-last entry (file + line + code): the exact place where the exception was born. It's the where.
- From there upwards: the chain of calls that led there (
<module>calledload_catalog, which ran theopen). It's the how we got here.most recent call lastmeans literally that: the most recent call is at the end.
| Part of the traceback | Question it answers | In the example |
|---|---|---|
Last line (Type: message) |
What failed? | data/catalog.json doesn't exist |
Last File ... line ... entry |
Where was it born? | Line 18, inside load_catalog |
| Entries above | Who called whom? | The main program (line 42) called load_catalog |
A traceback is not a punishment: it's the best free incident report in existence. Learning to read it calmly will save you hours; copying it in full (last line included) is also the right way to ask for help.
Syntax errors vs runtime exceptions
Not all red text is the same. There are two radically different families:
Syntax error (SyntaxError) |
Runtime exception | |
|---|---|---|
| When it appears | Before anything executes: when the file is compiled | Mid-execution, when the problematic line is reached |
| Depends on the data | No: it always fails, with any input | Yes: float(text) works with "12.50" and blows up with "free" |
| Can it be handled with this module's mechanism? | No: you fix the code and that's that | Yes: it's exactly what try/except (07-02) is for |
| Papyrus example | Forgetting the colon on an if |
The sales CSV brings a corrupt row |
The practical consequence matters: a SyntaxError is always your bug and is fixed by editing; a runtime exception may be a bug or it may be the real world behaving like the real world (missing files, users typing anything at all). This module deals with the second family.
The built-in exceptions Papyrus already knows (without knowing it)
Python ships with dozens of predefined exceptions. These seven cover 90% of everyday work, and every one of them already has an episode in the Papyrus story:
| Exception | When it's raised | Papyrus example |
|---|---|---|
ValueError |
The type is right but the value isn't acceptable | float("free") while reading sales.csv; Book's __post_init__ with price=-5 (M5) |
TypeError |
The data type isn't what the operation expects | "12.50" * 0.04 doesn't fail (it repeats text), but "12.50" + 0.04 does: you can't add str and float |
KeyError |
Missing key in a dictionary | catalog["faust "] — the trailing space breaks the key normalized with strip().casefold() (M4) |
IndexError |
Index out of range in a list or tuple | recent_sales[10] when there were only 3 sales today |
FileNotFoundError |
Attempting to open a file that doesn't exist | open(BASE / "data" / "catalog.json") on Ana's new laptop, where data/ doesn't exist yet (M6) |
ZeroDivisionError |
Division by zero | total_sales / number_of_sales in close_till() on a day with no sales |
AttributeError |
The object doesn't have that attribute or method | book.titel (a typo) instead of book.title; or calling .append() on the catalog dict |
It's worth triggering them on purpose in the interactive interpreter: type int("Hamlet"), {}["faust"], [][0]... Seeing each one's name and message in a controlled environment means you'll recognize them instantly when they show up in a real traceback.
>>> catalog = {"hamlet": "Book(Hamlet, 9.95, 6)"}
>>> catalog["Hamlet "] # we forgot to normalize the key
Traceback (most recent call last):
...
KeyError: 'Hamlet 'The KeyError message shows the key that was looked up with its quotes: that's where the treacherous space becomes visible. Another reason to read the last line with a magnifying glass.
The exception hierarchy: Exception as the mother class
Exceptions are classes, and like every Python class (we saw this in module 5) they can inherit. Almost all the ones you care about descend from Exception:
graph TD
BE[BaseException] --> KI[KeyboardInterrupt<br/>Ctrl+C]
BE --> EX[Exception]
EX --> AE[ArithmeticError] --> ZD[ZeroDivisionError]
EX --> LE[LookupError] --> KE[KeyError]
LE --> IE[IndexError]
EX --> VE[ValueError]
EX --> TE[TypeError]
EX --> ATE[AttributeError]
EX --> OS[OSError] --> FNF[FileNotFoundError]
Ideas worth retaining now (we'll put them to work in 07-02 and 07-04):
Exceptionis the mother of "normal" errors: the ones a program can reasonably handle.BaseExceptionsits above it and includes things you should almost never catch, such asKeyboardInterrupt(the Ctrl+C Ana uses to stop the program on purpose). That's why "catch everything" is a bad idea: you could even prevent the user from cancelling.- The intermediate categories group things:
KeyErrorandIndexErrorare bothLookupError("I looked something up and it wasn't there");FileNotFoundErroris anOSError(a problem with the operating system). Handling the category handles all its children — the same polymorphism from module 5, applied to errors. - In 07-04 we will extend this tree with Papyrus's own exceptions, hanging off
Exceptionexactly likeValueErrordoes.
EAFP vs LBYL: why Path.exists() was a band-aid
Module 6 called it a band-aid over and over. Now we can explain why with the proper vocabulary. There are two philosophies for dealing with operations that can fail:
- LBYL — Look Before You Leap: check the preconditions before acting.
if path.exists(): open(path). That's what we did in module 6. - EAFP — Easier to Ask Forgiveness than Permission: act directly and handle the exception if something goes wrong. This is Python's idiomatic style.
| Criterion | LBYL (if path.exists():) |
EAFP (try and handle) |
|---|---|---|
| Race condition | Time passes between the exists() and the open(): another process (the nightly backup!) can delete or move the file in between. The check guarantees nothing |
The open() is itself the check: it either works or reports, with no gap in between |
| Double work | The system looks up the file twice: once for exists(), again for open() |
A single operation |
| Coverage | exists() only covers "doesn't exist". What if it exists but is corrupt, or lacks read permission, or is a directory? You'd need an if per case |
The exception covers any failure, even the ones you didn't anticipate |
| Readability | The happy path is buried under checks | The happy path reads cleanly; error handling stands apart |
The race condition deserves a concrete example: Papyrus's nightly backup (make_backup(), M6) renames and moves files in data/. If register.py checks catalog_json.exists() → True, and a tenth of a second later the backup process moves that file, the subsequent open() blows up despite the check. LBYL gives a false sense of security: you verified a snapshot of the past, not the present.
Careful: EAFP does not mean "never validate". Validating input data (is this price Julia typed negative?) is still correct, and we'll refine it in 07-03. What EAFP discourages is trying to predict the state of the outside world (files, network, other processes) with prior checks, when the attempt itself already tells you the truth.
What we still don't know how to do is "ask forgiveness": handle the exception when it arrives. That's exactly the topic of the next lesson.
Common Mistakes and Tips
- Reading the traceback top-down and panicking by the second line. The other way around: last line first (type and message), then the point of origin, then the call chain. Bottom-up, always.
- Confusing a
SyntaxErrorwith a handleable exception. If the error appears before anything runs, it's syntax: you fix the code, you don't "handle" it. - Thinking
KeyError: 'Hamlet 'andKeyError: 'hamlet'are the same error. The message shows the exact key in quotes: spaces and capitalization included. In Papyrus, it almost always means somebody skipped thestrip().casefold()normalization. - Believing that
Path.exists()"fixes" the missing-file problem. It only covers one failure among many and leaves a time window open. It's a band-aid — module 6 said so, and now you know why. - Tip: when a traceback baffles you, reproduce the exception by hand in the interpreter with minimal data (
float("free"),{}["x"]). Isolating the failure from the big program is half the diagnosis. - Tip: keep the tracebacks you run into over the next few days. In 07-05 you'll learn to send them automatically to a log file instead of losing them when you close the terminal.
Exercises
-
Traceback detective. Without running anything, predict which exception (exact type) each snippet raises and why: (a)
int("12.50"); (b)catalog = {}followed bycatalog["hamlet"].price; (c)sales = []followed bysales[0]; (d)"Faust" + 21.00. Then check in the interpreter and compare the real message with your prediction. -
Traceback autopsy. Given this traceback, answer: what failed?, on what line and in what function was it born?, what was the complete call chain?, what piece of data triggered the failure?
Traceback (most recent call last): File "register.py", line 31, in <module> total = close_till() File "register.py", line 22, in close_till amounts.append(float(row["amount"])) ValueError: could not convert string to float: 'twelve fifty' -
Classify the philosophies. For each Papyrus situation, decide whether LBYL or EAFP fits better and justify it in one sentence: (a) opening
data/config.jsonat startup; (b) checking thatunitsis greater than 0 before selling; (c) accessingcatalog[key]with a key Julia typed; (d) checking thatdata/exists before creating it withmkdir.
Solutions
- (a)
ValueError: the type (str) is acceptable forint(), but the value"12.50"doesn't represent an integer —int()doesn't truncate decimal strings. (b)KeyError: 'hamlet': the dictionary is empty; note that the failure happens at the[...]access, before ever reaching.price. (c)IndexError: empty list, there is no position 0. (d)TypeError: you can't concatenatestrwithfloat; the real message sayscan only concatenate str (not "float") to str. - What: a
ValueErrorbecausefloat()received'twelve fifty', which can't be converted to a number. Where: line 22, insideclose_till. Chain: the main program (<module>, line 31) calledclose_till(), which on its line 22 tried to convert theamountfield of a CSV row. Guilty data: somebody wrote the amount in words insales.csv— exactly the "pray the CSV brings numbers" scenario from module 6. - (a) EAFP: it's outside-world state; attempting the open and handling
FileNotFoundErroravoids the race condition and covers more failures thanexists(). (b) LBYL (input validation): it's data you already have in hand; checking beforehand is cheap, with no time window — in 07-03 we'll see that the correct reaction to that check israise. (c) EAFP:trywith direct access (or.get(), which we saw in M4) is more Pythonic thanif key in catalogfollowed by the access — two lookups instead of one. (d) Trick question: neither —path.mkdir(exist_ok=True)(M6) removes the problem at the root; the best check is the one you don't need.
Conclusion
You now have the map of the territory: an exception is an object that interrupts the flow and propagates upwards until somebody catches it or the program dies; the traceback is its forensic report and is read bottom-up; syntax errors are a different species (you fix them, you don't handle them); the built-in exceptions form a class hierarchy with Exception as the mother, and Python prefers EAFP — try it and ask forgiveness — over the LBYL of Path.exists(), which leaves race conditions open and only covers the failures you knew to anticipate. For now, though, we're still where Ana is: we see the punch coming and don't know how to take it. The next lesson introduces the central tool of the whole module, try/except, and with it we'll settle two historic debts: load_catalog() will stop depending on catalog.json existing, and the int(input()) that has been dragging its ValueError along since module 1 will finally get proper validation.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
