07 - Errors and Exceptions¶
What this session is¶
About an hour. You'll learn how Python handles things going wrong - files that don't exist, numbers that can't be parsed, keys missing from dicts. Python's model is exceptions: when something goes wrong, the language throws an exception that flies up the call stack until someone catches it.
Heads-up if you've come from a Go-style language: Python is the opposite. Functions don't return error values; they raise exceptions. The discipline is different.
A small example¶
Run it. Python prints something like:
Traceback (most recent call last):
File "test.py", line 1, in <module>
n = int("hello")
~~~^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'hello'
ValueError is the type of exception. The error message says what happened. The "Traceback" shows the call chain that led to it. Read tracebacks bottom-up - the last line is what actually failed; the lines above show how you got there.
If nothing catches the exception, the program crashes.
Catching exceptions: try/except¶
try:
n = int(input("Enter a number: "))
print(f"You entered {n}")
except ValueError:
print("That wasn't a number.")
How it reads:
- Try the code in the try: block.
- If it raises ValueError, jump to the except ValueError: block.
- If no exception, skip the except.
Run it. Enter 42 - get "You entered 42". Run again, enter hello - get "That wasn't a number."
Catching multiple exception types¶
try:
risky_thing()
except ValueError:
handle_bad_value()
except (FileNotFoundError, PermissionError):
handle_io_problem()
except Exception as e:
print(f"Something else went wrong: {e}")
Patterns:
- Multiple except clauses - first match wins (top to bottom).
- Group exceptions in a tuple: except (A, B):.
- as e captures the exception object - you can print it, log it, inspect it.
- Exception is the catch-all. Use sparingly (you might swallow bugs you'd rather see).
The trap
Don't catch bare except: (no exception type). It also catches KeyboardInterrupt (Ctrl-C) and SystemExit - which means your program won't exit cleanly and Ctrl-C won't kill it. Always at minimum: except Exception:.
The full shape: try/except/else/finally¶
try:
f = open("data.txt")
except FileNotFoundError:
print("file not found")
else:
# runs only if try succeeded with no exception
contents = f.read()
f.close()
finally:
# runs no matter what (success, handled exception, unhandled exception)
print("cleaning up")
In practice you'll mostly write try/except, sometimes try/except/finally. The else clause is occasionally useful - it lets you separate "the success path" from the try block clearly.
with statements: the modern way to clean up¶
The try/finally for "open a resource, use it, close it" gets old. Python's context managers + with statement automate the cleanup:
with open("data.txt") as f:
contents = f.read()
# file is automatically closed here, even if an exception occurred
Reads as: "open data.txt as f, use it inside, automatically close when leaving the block." Any object that supports the with protocol (called a "context manager") works. You'll see it for files, network connections, database transactions, locks. Use with whenever you have a "must clean up after" resource.
Common exception types¶
You'll meet these often:
| Exception | Meaning |
|---|---|
ValueError |
wrong value (e.g., int("hello")) |
TypeError |
wrong type (e.g., len(42)) |
KeyError |
dict key missing |
IndexError |
list index out of range |
AttributeError |
object doesn't have that attribute |
FileNotFoundError |
file doesn't exist |
ZeroDivisionError |
divided by zero |
RuntimeError |
generic "something went wrong" |
NotImplementedError |
placeholder for "I haven't written this yet" |
Exception |
base of all the above (catch-all) |
When in doubt, look at the traceback's last line - it names the type.
Raising your own exceptions¶
You can throw an exception with raise:
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"can't withdraw {amount} from {balance}")
return balance - amount
new_balance = withdraw(100, 200) # raises ValueError
Use existing exception types when they fit (ValueError, TypeError, KeyError). Create your own when you want callers to catch this specific kind of failure:
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(f"can't withdraw {amount} from {balance}")
return balance - amount
try:
withdraw(100, 200)
except InsufficientFundsError as e:
print(f"transaction declined: {e}")
Custom exception classes inherit from Exception. The pass body means "no extra code; just be a distinct type."
Re-raising¶
Sometimes you want to catch, do something, then re-raise:
try:
risky()
except ValueError as e:
log.error(f"value error in risky(): {e}")
raise # re-raise the same exception, preserving the traceback
A bare raise inside an except block re-raises. Use when you want to add logging/context but not change the failure path.
EAFP vs LBYL¶
A Python idiom worth knowing: EAFP - "Easier to Ask Forgiveness than Permission."
The contrast: LBYL ("Look Before You Leap") - check preconditions, then do.
# LBYL - check first
if "Alice" in ages:
print(ages["Alice"])
else:
print("not found")
# EAFP - try, catch failure
try:
print(ages["Alice"])
except KeyError:
print("not found")
Both work. The Pythonic preference is EAFP for most cases - it's faster (no double lookup) and handles race conditions better (the key might disappear between the check and the use). LBYL is fine when the check is cheap and clearer to read.
A real example: safely parse an integer¶
def parse_positive(s: str) -> int:
"""Parse s as a positive integer. Raise ValueError on failure."""
try:
n = int(s)
except ValueError:
raise ValueError(f"not a number: {s!r}")
if n <= 0:
raise ValueError(f"must be positive: {n}")
return n
Notice: we catch ValueError, then raise our own ValueError with a better message. The caller still sees a ValueError - but with context that the underlying int("hello") failure didn't have.
Exercise¶
In a new file parse.py:
-
Write
parse_positive(s: str) -> int(above). -
In the main script, loop over these inputs and call
parse_positiveon each. For each, print either the parsed number or the error message: -
Use
try/except. Print like:42 -> 42for success,hello -> error: not a number: 'hello'for failure.
Expected output:
42 -> 42
hello -> error: not a number: 'hello'
-5 -> error: must be positive: -5
0 -> error: must be positive: 0
100 -> 100
- Stretch: write a custom exception
BadInputError(Exception). Haveparse_positiveraise it instead ofValueError. Update the loop'sexceptto catchBadInputError.
What you might wonder¶
"Should I always wrap things in try/except, just in case?"
No. The Pythonic approach is to catch only what you can meaningfully recover from. try: x = int(input()) except ValueError: prompt_again() makes sense. try: x = 1 + 1 except Exception: is just noise - there's nothing to recover from. Let unexpected exceptions propagate and crash the program loudly; that's how you find bugs.
"What about assert?"
assert condition raises AssertionError if the condition is false. Useful for "this should never happen - if it does, fail loudly so I notice." Not for input validation - assertions can be disabled with python -O (optimizations on), and you don't want validation to disappear in production.
"Why does Python use exceptions instead of error values like Go does?" Design choice. Exceptions hide control flow (a function call may secretly jump to a handler 10 frames up). The trade: less code in the happy path, but harder to see all failure paths. Different philosophies - Python's been exceptions-first since 1991. You learn it.
"What's the traceback chain thing I sometimes see?"
If an exception happens inside an except block, Python prints both. The default link is "during handling of the above exception, another exception occurred." Useful for debugging cascading failures. You can also explicitly chain with raise NewError(...) from old_error.
Done¶
You can now:
- Recognize exceptions and read tracebacks (bottom-up).
- Catch exceptions with try/except.
- Use try/except/else/finally correctly.
- Use with statements for automatic cleanup.
- Raise your own exceptions.
- Define custom exception types.
- Know the EAFP idiom (try, catch failure) vs LBYL.
You've now seen Python's distinctive failure-handling idiom. Real Python code is mostly: data shaping, control flow, function composition, exception handling, and the things on the next few pages.
Next page: Python's most distinctive positive feature - iterators, generators, and comprehensions.
Next: Iterators, generators, comprehensions →