Python Error Handling with try and except
A practical guide to error handling in Python — catching specific exceptions, the else and finally clauses, raising and re-raising, custom exception types, and habits that lead to robust code.
What you'll learn
- ✓The shape of try / except / else / finally
- ✓Catching specific exceptions instead of bare except
- ✓Raising and re-raising exceptions
- ✓Defining your own exception types
- ✓When to handle and when to let it crash
Prerequisites
- •Basic Python familiarity
Errors are not exotic events in Python — they are a normal part of program flow. Files don’t exist, networks drop, users type “five” into a number field. The try and except statements give you a precise vocabulary for saying which errors you expect, where you can recover, and which ones should bring the program down. Used well, they make code robust. Used poorly, they hide bugs forever.
The Basic Shape
try:
value = int(user_input)
except ValueError:
print("That wasn't a number, please try again.")
If int() raises ValueError, the except block runs. If it succeeds, the except block is skipped. Either way, execution continues after the try statement.
Catch Specific Exceptions
The single most important rule of error handling: catch the narrowest exception you can. A bare except: catches everything, including KeyboardInterrupt and SystemExit, which means Ctrl-C may stop working and bugs vanish without a trace.
# Don't do this
try:
do_something()
except:
pass
# Better
try:
do_something()
except ValueError as e:
log.warning("Skipping bad input: %s", e)
except Exception is almost as bad — it swallows every standard error type. Catch the specific exceptions your code might actually produce.
Catching Multiple Types
You can list several exception types in a tuple.
try:
parse(payload)
except (ValueError, KeyError) as e:
log.warning("Bad payload: %s", e)
Or you can use multiple except clauses for different recovery strategies.
try:
response = fetch(url)
except TimeoutError:
schedule_retry(url)
except ConnectionError:
mark_offline(url)
else: When the try Succeeded
The else clause runs only if the try block did not raise. It is the right place for code that should run after a successful operation but that you don’t want to wrap in the try itself.
try:
data = json.loads(payload)
except json.JSONDecodeError:
log.warning("Invalid JSON")
else:
process(data) # only runs if loads() succeeded
Why bother? If process(data) accidentally raises json.JSONDecodeError too, the except clause would catch it incorrectly. Keeping the try body small protects against this.
finally: Always Run
The finally clause runs whether or not an exception happened — even if you return from inside the try. It is the standard place for cleanup.
f = open("data.txt")
try:
process(f)
finally:
f.close()
In practice, with statements (context managers) have replaced most uses of finally for resource cleanup, but finally is still useful when you can’t or don’t want to write a context manager.
Raising Exceptions
To signal an error, raise an exception with raise.
def withdraw(account, amount):
if amount > account.balance:
raise ValueError("Insufficient funds")
account.balance -= amount
Choose a built-in exception type that matches the error: ValueError for bad arguments, TypeError for wrong types, KeyError for missing dict keys, FileNotFoundError for missing files, and so on.
Re-raising and Wrapping
Sometimes you want to catch an exception, do something useful, then let it propagate.
try:
risky()
except ValueError:
log.exception("risky() failed")
raise # re-raise the same exception
A bare raise inside an except block re-raises the current exception with its original traceback intact. This is almost always what you want.
If you need to convert an exception to a different type — for example, hiding a low-level driver error behind a friendly application error — chain them with raise ... from ....
try:
row = db.query(sql)
except DriverError as e:
raise DataAccessError("Lookup failed") from e
The from clause preserves the original cause in the traceback, so you don’t lose information.
Custom Exception Types
Defining your own exception types is cheap and dramatically improves error handling further up the stack.
class PaymentError(Exception):
pass
class InsufficientFunds(PaymentError):
pass
class CardDeclined(PaymentError):
pass
Callers can now write except PaymentError to catch any payment problem, or except InsufficientFunds to handle just the one they know how to recover from. The hierarchy is the point.
Don’t Use Exceptions for Control Flow
Exceptions are slow on the raising path and confusing for readers. If a condition is part of normal flow — for example, checking whether a key exists — prefer if over try.
# Awkward
try:
name = user["name"]
except KeyError:
name = "guest"
# Clearer
name = user.get("name", "guest")
The general rule: use exceptions for exceptional cases. Use conditionals for things you expect to happen routinely.
Logging in except Blocks
Inside an except block, prefer log.exception("...") to log.error("..."). The former automatically attaches the traceback, which makes debugging dramatically easier.
try:
work()
except Exception:
log.exception("work() failed")
raise
When to Let It Crash
The hardest part of error handling is knowing when not to. If you can’t actually recover from an error — say, your database connection is permanently broken — catching the exception just to log it and continue produces a silently broken program. Better to let it crash, get a clear traceback, and fix the cause.
The “catch and ignore” pattern is one of the biggest sources of mysterious bugs in real-world Python. Every except block should answer the question: what does this code do to make things better?
Wrapping Up
Good error handling is mostly about discipline: catch specific exceptions, keep try blocks short, use else and finally for clarity, raise meaningful types, and let the unfixable crash. With those habits, your code degrades gracefully where it can and fails loudly where it must.
Related articles
- Java Java Exception Handling Best Practices
Clear rules for using checked and unchecked exceptions in Java, with patterns for wrapping, logging, and propagating errors in real production code.
- Python Python asyncio Event Loop Guide
Understand how Python's asyncio event loop schedules coroutines, what await actually does, and how to avoid the classic mistakes that turn async code into a tangle of bugs.
- Python Python Decorators Deep Dive
A practical tour of Python decorators: how they work under the hood, when to use them, and how to write decorators that preserve metadata, accept arguments, and stack cleanly.
- Python Python Logging Best Practices
How to set up Python logging properly: loggers vs handlers, structured logs, contextual fields, log levels that scale, and how to avoid the classic print-debug trap.