What is it? #
An exception is Python's way of saying "I cannot continue with this line". A missing file, a bad conversion, a network timeout — each raises a specific exception type.
If nothing handles it, the exception travels up through the calling functions and the program stops with a traceback. That is not always bad. A crash with a clear message beats a program that silently produced wrong numbers.
try/except lets you deal with the failures you expect and can do something about. The key word is expect. Catching everything, everywhere, is how bugs get buried.
You can also raise your own exceptions with raise, which is how you signal that a rule of your own has been broken.
Think of it like this #
Think of a delivery driver. If an address does not exist, they do not invent one and drop the parcel anywhere. They report the specific problem: address not found. Your code should do the same — catch the specific problem you know how to handle, and let the unknown ones be reported honestly.
Simple example #
You are reading a settings file and converting a value to a number. The file may be missing, the key may be absent, and the value may not be a number. Each failure needs a different response.
Code #
def read_timeout(path):
try:
with open(path) as f:
raw = f.read().strip()
except FileNotFoundError:
print("No config file, using default")
return 30
except PermissionError as exc:
raise RuntimeError(f"Cannot read {path}") from exc
try:
value = int(raw)
except ValueError:
print(f"Bad timeout value: {raw!r}, using default")
return 30
else:
print("Config read successfully")
return value
finally:
print("Finished reading config")
print(read_timeout("missing.txt"))
# Raising your own
class InsufficientBalance(Exception):
"""Raised when an account cannot cover a withdrawal."""
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > balance:
raise InsufficientBalance(f"Short by {amount - balance}")
return balance - amount
try:
withdraw(500, 900)
except InsufficientBalance as exc:
print("Declined:", exc)
How it works #
The try block holds the code that might fail. Python runs it, and if an exception occurs it looks for a matching except.
except FileNotFoundError catches only that type. Being specific is the whole point: if the file is missing you have a sensible default, but if the disk is failing you want to know.
raise RuntimeError(...) from exc re-raises a different error while keeping the original attached. The traceback then shows both — what went wrong and what it was doing at the time.
else runs only when the try block finished without an exception. It keeps the "everything worked" path out of the try, so you do not accidentally catch exceptions from the success path.
finally runs no matter what — success, handled failure, or an exception on its way up. It is for cleanup: closing things, releasing locks, logging that the attempt ended.
with open(path) as f is worth noticing here. The with statement closes the file even if an exception is raised inside the block, so you rarely need finally just for closing files.
The custom exception at the bottom is a plain class inheriting from Exception. Giving your domain failures their own type means callers can catch exactly the case they care about, instead of parsing error strings.
Real-world use #
In a web application, exception handling decides what the user sees. A ValidationError becomes a 400 with a helpful message; an unexpected KeyError becomes a 500 and a log entry someone gets paged about. Mixing those two up is how bugs go unnoticed for months.
Anything that touches the network needs exception handling by default — timeouts, connection resets and DNS failures are normal, not exceptional, at scale.
The opposite mistake is just as common: a bare except: around a big block, which swallows typos, keyboard interrupts and real bugs alike. If you catch something, you should be able to say what you will do about it.
Common mistakes #
- Writing a bare
except:orexcept Exception:around everything. It hides bugs you would rather see. - Catching an exception and doing nothing (
pass). If it is truly ignorable, say so in a comment and log it. - Putting the whole function inside one giant
try. Wrap only the lines that can actually fail. - Using exceptions for normal flow control, like raising StopProcessing to break a loop.
- Losing the original error when re-raising. Use
raise NewError(...) from excso the traceback keeps the cause.
Practice #
Write safe_divide(a, b) that returns the result, returns None on division by zero with a printed warning, and raises a TypeError with a clear message when either input is not a number. Then write a small loop that calls it with several inputs, including bad ones, and keeps running.