S4.0 What This Teaches
This tutorial covers Python exception handling:
try / except / else / finally
- Built-in exception types and the exception hierarchy
- Raising exceptions with
raise
- Custom exception classes
- Exception chaining with
raise ... from
- Context managers and
with for cleanup
Rust uses Result<T, E> and ? for recoverable errors
instead of exceptions. Python's philosophy ("Easier to Ask Forgiveness than
Permission", or EAFP) embraces try/except more readily than C#.
S4.1 try / except / else / finally
try:
x = int("abc") # raises ValueError
y = 10 // 0 # raises ZeroDivisionError
except ValueError as e:
print(f"Value error: {e}")
except ZeroDivisionError:
print("Cannot divide by zero")
except Exception as e: # catch-all for unexpected exceptions
print(f"Unexpected: {type(e).__name__}")
else:
print("no exception occurred") # only runs if no exception was raised
finally:
print("always runs")
The else clause runs when the try block completed
without raising - useful to separate success logic from error handling.
S4.2 Built-in Exception Types
| Exception | Common cause |
ValueError | Argument has right type but bad value |
TypeError | Wrong type passed to an operation |
KeyError | Dict key not found |
IndexError | List or string index out of range |
AttributeError | Object doesn't have the requested attribute |
FileNotFoundError | File or directory doesn't exist |
ZeroDivisionError | Division or modulo by zero |
StopIteration | Iterator has no more items |
OSError | OS-level error (file, network, permissions) |
RuntimeError | General-purpose error when no better type fits |
S4.3 Raising Exceptions
def square_root(x: float) -> float:
if x < 0:
raise ValueError(f"square_root requires x >= 0, got {x}")
return x ** 0.5
# Re-raise the current exception
try:
do_work()
except SomeError:
log("error occurred")
raise # re-raises without losing the original traceback
# Exception chaining
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise ValueError("bad config file") from e
S4.4 Custom Exception Classes
class InsufficientFundsError(Exception):
def __init__(self, amount: float, balance: float):
self.amount = amount
self.balance = balance
super().__init__(
f"Cannot withdraw {amount:.2f}; balance is {balance:.2f}"
)
# Usage
try:
raise InsufficientFundsError(100.0, 50.0)
except InsufficientFundsError as e:
print(e) # Cannot withdraw 100.00; balance is 50.00
print(f"Short by {e.amount - e.balance:.2f}")
Derive custom exceptions from Exception (not BaseException).
Give them a descriptive name ending in Error.
S4.5 Context Managers and with
The with statement calls __exit__ on the context manager
when the block ends - even if an exception is raised. This guarantees cleanup:
# File opened and closed even if an exception occurs
with open("data.txt") as f:
for line in f:
print(line.strip())
# Multiple context managers on one line
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
# contextlib.contextmanager for custom context managers
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.monotonic()
yield
print(f"elapsed: {time.monotonic() - start:.3f}s")
with timer():
sum(range(10_000_000))
S4.6 Example - All Together
# Exceptions - safe integer parsing and custom validation error.
class ValidationError(ValueError):
def __init__(self, field: str, message: str):
self.field = field
super().__init__(f"{field}: {message}")
def parse_age(text: str) -> int:
try:
age = int(text)
except ValueError:
raise ValidationError("age", f"'{text}' is not an integer") from None
if not 0 <= age <= 150:
raise ValidationError("age", f"{age} is outside valid range 0-150")
return age
for value in ["25", "abc", "200"]:
try:
print(f"age: {parse_age(value)}")
except ValidationError as e:
print(f"error [{e.field}]: {e}")
Expected output:
age: 25
error [age]: age: 'abc' is not an integer
error [age]: age: 200 is outside valid range 0-150
S4.7 Exercise
Exercise
- Write a function
safe_divide(a, b) that returns None
instead of raising ZeroDivisionError.
- Create a custom
ConfigError exception. Write a function that
reads a config dict and raises ConfigError when a required key
is missing.
- Use
try/except/else/finally to open a file: print its line
count in else, handle FileNotFoundError in
except, and print "done" in finally.
S4.8 Common Mistakes
Bare except: catches everything including KeyboardInterrupt
try:
do_work()
except: # catches SystemExit, KeyboardInterrupt, etc!
pass
except Exception: # correct - catches application-level exceptions only
pass
Silencing exceptions with pass
try:
result = risky()
except Exception:
pass # error swallowed - caller never knows it failed
At minimum log the exception. Only silence it if you have a concrete reason
the error is expected and harmless.
Raising a new exception instead of re-raising
except ValueError as e:
raise ValueError(str(e)) # creates new exception, loses original traceback
raise # correct: re-raises with original context
S4.9 Key Terms
| Term | Meaning |
| try / except | Block pair catching exceptions of a specified type |
| else | Runs after try only if no exception was raised |
| finally | Runs always; used for cleanup independent of exception status |
| raise | Throws an exception; bare raise re-raises the current one |
| raise ... from | Chains exceptions: new exception records the cause |
| Exception | Base class for application-level exceptions; all built-ins derive from it |
| context manager | Object implementing __enter__/__exit__; used with with statement |
| with | Ensures __exit__ (cleanup) is called even if an exception occurs |