Programs fail. Python's exception model lets you handle failures cleanly instead of crashing. The with statement runs setup and teardown code automatically, which is the standard way to manage files, network connections, and locks. This lesson covers both, plus how to write your own context manager and how to inspect the traceback when something goes wrong.
The try, except, else, finally chain
Wrap risky code in try. If an exception fires, the matching except block runs. The optional else runs when no exception was raised. finally always runs, which makes it perfect for cleanup like closing a file or releasing a lock.
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
return "cannot divide by zero"
except TypeError as e:
return f"bad type: {e}"
else:
return f"{a} / {b} = {result}"
finally:
print("divide attempt finished")
print(safe_divide(10, 2)) # 10 / 2 = 5.0
print(safe_divide(10, 0)) # cannot divide by zero
Raising your own exceptions
Use raise to signal an error. Create custom exception classes for domain-specific problems. Raising an exception immediately unwinds the call stack until a matching except block catches it.
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
super().__init__(f"need {amount}, have {balance}")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount < 0:
raise ValueError("amount must be positive")
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
withdraw(100, 200)
except InsufficientFundsError as e:
print(f"blocked: {e}")
The with statement and context managers
A context manager is an object that defines __enter__ and __exit__. The with statement calls __enter__ on entry and __exit__ on exit, even if an exception fires. Files are the most familiar example.
# Reading a file the safe way
with open("notes.txt", "r", encoding="utf-8") as f:
contents = f.read()
# f is now closed, even if an exception fired inside the with block
Write your own context manager with contextlib
Use contextlib.contextmanager to turn a generator into a context manager. Code before yield runs on entry, code after yield runs on exit, and the yield expression's value is bound to the as variable.
import contextlib, time
@contextlib.contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")
with timer("big loop"):
total = sum(range(10_000_000))
print(total) # 49999995000000
Reading and using the traceback module
When an unhandled exception crashes a program, Python prints a traceback that shows the call chain. The traceback module gives you programmatic access. Use traceback.print_exc() in an except block to log the full chain.
import traceback
def divide(a, b):
return a / b
try:
divide(1, 0)
except ZeroDivisionError:
traceback.print_exc()
# Traceback (most recent call last):
# File "<stdin>", line 4, in <module>
# divide(1, 0)
# File "<stdin>", line 2, in divide
# return a / b
# ZeroDivisionError: division by zero
Common pitfalls with exceptions
Three traps repeat across codebases. First, a bare except: clause catches every exception including SystemExit and KeyboardInterrupt, which makes graceful shutdown impossible. Always specify an exception class, or use except Exception: as a slightly safer default. Second, swallowing an exception silently with a pass hides real bugs. If you really must catch and ignore, log the exception with traceback.print_exc() or the logging module so future-you can find the cause. Third, using exceptions for control flow is slow because raising and catching is expensive in CPython. For predictable hot paths, check the condition (if key in dict:) instead of asking forgiveness (try: dict[key] except KeyError:). The EAFP pattern is fine when failures are rare; LBYL is better when the check is cheap and the failure common.
Comparison with JavaScript try/catch and Java checked exceptions
If you come from JavaScript, Python's try/except looks familiar but the semantics are richer. JavaScript try/catch supports a single catch block; Python allows many except clauses plus else and finally. JavaScript also has no equivalent of Python's context managers, so every resource needs an explicit try/finally cleanup. C++ and Java go the other way: Java has checked exceptions that the compiler forces you to handle, which makes the call site explicit but produces noise. Python deliberately rejects checked exceptions: any function may raise anything, and the caller decides what to catch. The trade-off is fewer surprises in interfaces (no throws clause to keep in sync with the body) at the cost of less compile-time information. Java's checked exceptions have aged poorly in modern code, and most teams prefer Python's lighter model.
Best practices for exception handling in 2026
Three habits keep exception-heavy code maintainable. First, define a small hierarchy of custom exceptions at the top of your module, derived from a single project base class. Callers can then catch the project base to handle everything your library raises. Second, attach context to exceptions when re-raising: raise InsufficientFundsError(...) from original_error preserves the original cause in the traceback, while raise NewError(...) loses it. Third, prefer structured logging (the logging module with JSON formatter) over printing tracebacks. A single except Exception: logger.exception("operation failed: %s", operation) gives you the type, the message, and the full stack trace in one log line, ready for any aggregator to parse.
Summary
Use try/except to handle errors, raise to signal them, and the with statement to manage resources. contextlib.contextmanager lets you write context managers as small generators. The next lesson covers reading and writing files, JSON and CSV data, and working with paths.