Exceptions
try, except, else, finally, raising, and writing your own exception classes
When something goes wrong, Python raises an exception. If nothing catches it, the program prints a traceback and exits. Catching is done with try / except.
Exceptions are Python's way of encoding expected failure paths: file not found, network down, invalid input, permission denied. They let you separate "happy path" logic from error handling.
What happens when exceptions are not handled?
If an exception is raised and no try/except catches it, Python does three things:
- Prints a traceback showing the call stack (which function called which).
- Exits the program with a non-zero exit code (signals failure to the shell).
- (In a web context) Returns a 500 Internal Server Error to the client (or crashes the worker process, depending on the framework).
The traceback shows the exception propagation up the call stack: main() called divide(), which raised ZeroDivisionError. Since main() didn't catch it, it propagated up to the caller (in this case, our outer try block).
In production, unhandled exceptions are bugs. Your job is to anticipate failure modes (try opening a file; it might not exist) and handle them gracefully.
Real-world: exceptions encode expected failures
Exceptions are not like Java checked exceptions (which force you to declare every possible error). They are Python's way of saying "this can fail, and you should decide what to do."
Common patterns:
- File I/O:
FileNotFoundError,PermissionError,OSError - Network:
requests.ConnectionError,requests.Timeout,socket.timeout - Parsing:
json.JSONDecodeError,ValueError(fromint("oops")) - APIs: Custom exceptions like
stripe.error.RateLimitError
You choose which to catch. If you catch FileNotFoundError, you're saying "I expect this file might not exist, and I know what to do." If you don't catch it, you're saying "if the file doesn't exist, the program should crash."
This is the EAFP philosophy: Easier to Ask for Forgiveness than Permission.
# LBYL (Look Before You Leap) — Pythonic but verbose
if os.path.exists(path):
with open(path) as f:
data = f.read()
else:
data = None
# EAFP (Easier to Ask for Forgiveness than Permission) — Pythonic
try:
with open(path) as f:
data = f.read()
except FileNotFoundError:
data = NoneThe second is more concise and handles race conditions (file deleted between exists check and open call).
Exception propagation up the call stack
When an exception is raised, Python unwinds the stack, looking for a try/except that handles it.
Each frame in the traceback is one step in this unwinding.
The simplest form
as e binds the exception instance so you can inspect it (type, message, attributes).
Exception objects have attributes
Every exception is an object. You can inspect its type (type(e)), its message (str(e)), and sometimes custom attributes (like e.errno for OSError).
Catching multiple types
Parentheses group multiple exception types. The first matching except block wins.
Order matters
If you have multiple except blocks, put the most specific first. except Exception will catch everything, so if you put it first, more specific blocks below are unreachable.
else and finally
The full shape of the statement is:
try:
... # the risky operation
except SomeError as e:
... # runs if SomeError is raised
else:
... # runs only if NO exception was raised
finally:
... # always runs, error or notelse is useful when you only want code to run when the try block succeeded. finally is for cleanup that must happen either way (closing files, releasing locks, restoring state).
finally always runs
finally runs no matter what: success, exception, return, break, continue, even sys.exit(). It is the only way to guarantee cleanup. Use it for closing files, releasing locks, restoring state, etc.
Raising exceptions
Use raise to signal an error from your own code. Use a specific built-in type when one fits; otherwise create your own.
Which exception type to raise?
ValueError— Right type, wrong value (int("oops"),math.sqrt(-1))TypeError— Wrong type entirely (len(5))KeyError/IndexError— Missing dict key / list indexAttributeError— Missing attributeFileNotFoundError/PermissionError— Filesystem troubleRuntimeError— Generic "something else went wrong"- Custom exception — Domain-specific error (
InsufficientFundsError,RateLimitExceeded)
raise ... from ... to preserve cause
When you wrap a lower-level error in a higher-level one, use from to keep the chain visible in the traceback.
Without from, the original ValueError is hidden. With from, the traceback shows both exceptions and their relationship.
raise from vs bare raise
raise NewError(...) from e— Wrapsein a higher-level error. Both appear in the traceback, withfrommarking the causal relationship.raise NewError(...)— Replacesewith a new error. The original is lost (unless you manually include its message).raise(no arguments) — Re-raises the current exception. Useful inexceptblocks when you want to log and re-raise.
Custom exception classes
Make your own exception types by subclassing Exception (or a more specific built-in). This lets callers catch your errors precisely.
Custom exceptions make your code self-documenting: except InsufficientFundsError is clearer than except ValueError (which could mean anything).
Exception hierarchy
Every exception inherits from BaseException. The ones you usually care about inherit from Exception (which itself inherits from BaseException). A few useful built-ins:
ValueError– right type, wrong value (int("oops"))TypeError– wrong type entirely (len(5))KeyError/IndexError– missing dict key / list indexAttributeError– missing attributeFileNotFoundError/OSError– filesystem troubleRuntimeError– generic, "something else went wrong"StopIteration– an iterator is exhausted
Exception catches "every normal error". Do not catch BaseException casually; it includes KeyboardInterrupt and SystemExit, which you usually want to let through.
Never catch BaseException casually
except BaseException will catch KeyboardInterrupt (Ctrl+C) and SystemExit (from sys.exit()), making your program impossible to kill. Always catch Exception or more specific types.
Bare except: is almost always a bug
try:
...
except: # catches everything, including KeyboardInterrupt
passThis silences errors you did not intend to handle and makes debugging miserable. Always be specific.
Bare except: antipattern
Never write except: without a type. It catches everything, including KeyboardInterrupt, SystemExit, and MemoryError. If you truly want to catch all normal errors, use except Exception:. But even that is usually too broad; prefer specific types.
EAFP vs LBYL philosophy
Python culture strongly prefers EAFP (Easier to Ask for Forgiveness than Permission) over LBYL (Look Before You Leap):
EAFP is often faster (one operation instead of two) and handles race conditions (state can change between check and use).
When to use LBYL
Use LBYL when the check is cheap and the exception is expensive (rare). Use EAFP when the happy path is common and exceptions are the exception. For most Python code, EAFP is idiomatic.
Challenges
Define a function to_int(text, default=0) that returns int(text) if possible, otherwise default. It should not propagate any exception to the caller.
Define a class InsufficientFundsError that inherits from Exception, then define a function withdraw(balance, amount) that returns the new balance after withdrawing amount, or raises InsufficientFundsError if amount > balance. amount must be non-negative; raise ValueError otherwise.
Define a function safe_divide(a, b, default=None) that returns a / b if possible, otherwise default. Catch both ZeroDivisionError and TypeError.
Define a decorator retry_on_rate_limit(max_retries=3) that catches a custom RateLimitError exception and retries the function up to max_retries times. If all retries fail, re-raise the exception.
Define RateLimitError as a custom exception class.
For testing, assume a function that raises RateLimitError on the first n-1 calls and succeeds on the nth call.
Multiple choice questions
Which block runs only when the try block raises no exception?
except
else
finally
The block after the try statement entirely
What is wrong with except: (bare except)?
It is invalid syntax.
It catches KeyboardInterrupt and SystemExit, making the program hard to kill.
It only catches Exception, which is too broad.
It requires an as e clause.
When should you use raise ... from e?
When you want to suppress the original exception.
When wrapping a lower-level exception in a higher-level one, to preserve the cause.
When re-raising the same exception.
When you want to catch multiple exception types.
What does finally do?
Runs only if an exception is raised.
Runs only if no exception is raised.
Runs no matter what: success, exception, return, or even sys.exit().
Suppresses the exception.
What is the EAFP philosophy?
Always check if a file exists before opening it.
Try the operation and catch the exception if it fails.
Never use exceptions; they are expensive.
Use assertions instead of exceptions.
What happens if an exception is raised and not caught?
The exception is silently ignored.
Python prints a traceback and exits with a non-zero exit code.
The exception is logged but the program continues.
The exception is converted to a warning.
Many exceptions come from I/O. Files are next.