Python Error Handling and Exceptions
Master Python exception handling — try/except/else/finally, custom exceptions, exception hierarchies, and debugging with pdb and logging.
Errors are inevitable. Python’s exception system lets you catch, handle, and recover from errors gracefully instead of crashing.
try / except / else / finally
try:
result = 10 / int(input("Enter a number: "))
except ZeroDivisionError:
print("Cannot divide by zero")
except ValueError:
print("That's not a valid number")
else:
print(f"Result: {result}") # runs only if no exception
finally:
print("Done") # always runs
| Block | When It Runs |
|---|---|
try |
Always first — code that might fail |
except |
When a matching exception occurs |
else |
When no exception occurred |
finally |
Always — cleanup code |
Catching Multiple Exceptions
try:
data = json.loads(raw_text)
value = data["key"] / data["divisor"]
except (KeyError, ZeroDivisionError) as e:
print(f"Data error: {e}")
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")
Catching Everything (Use Sparingly)
try:
risky_operation()
except Exception as e:
logger.exception("Unexpected error")
raise # re-raise after logging
Never use bare except: — it catches KeyboardInterrupt and SystemExit too.
Raising Exceptions
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount}, balance is {balance}"
)
return balance - amount
Custom Exception Hierarchies
class AppError(Exception):
"""Base exception for this application."""
pass
class ValidationError(AppError):
pass
class NotFoundError(AppError):
pass
class InsufficientFundsError(AppError):
pass
# Catch all app errors at API boundary
try:
process_request(data)
except AppError as e:
return error_response(str(e), status=400)
Exception Chaining
Preserve the original cause when wrapping exceptions:
try:
config = json.loads(read_file("config.json"))
except json.JSONDecodeError as e:
raise ConfigurationError("Invalid config file") from e
Common Built-In Exceptions
| Exception | Cause |
|---|---|
ValueError |
Wrong value, right type |
TypeError |
Wrong type for operation |
KeyError |
Missing dict key |
IndexError |
List index out of range |
FileNotFoundError |
File doesn’t exist |
AttributeError |
Missing attribute/method |
ImportError |
Module not found |
RuntimeError |
General runtime failure |
Assertions
For conditions that should never happen in correct code:
def calculate_discount(price, percent):
assert 0 <= percent <= 100, "Percent must be 0-100"
assert price >= 0, "Price cannot be negative"
return price * (1 - percent / 100)
Disable assertions in production with python -O. Use exceptions for user-facing validation.
Debugging with pdb
import pdb
def buggy_function(data):
total = 0
pdb.set_trace() # breakpoint — interactive debugger starts here
for item in data:
total += item["value"]
return total
pdb commands: n (next), s (step into), c (continue), p variable (print), q (quit).
Python 3.7+: use breakpoint() instead of pdb.set_trace().
Debugging with logging
Prefer logging over print for production debugging:
import logging
logger = logging.getLogger(__name__)
def process(items):
logger.debug("Processing %d items", len(items))
for i, item in enumerate(items):
try:
transform(item)
except Exception:
logger.exception("Failed on item %d: %r", i, item)
raise
Context Managers for Cleanup
Guarantee cleanup even when errors occur:
with open("data.txt") as f:
process(f.read())
# file closed even if process() raises
with db.transaction():
db.execute("INSERT ...")
db.execute("UPDATE ...")
# committed or rolled back automatically
Best Practices
- Catch specific exceptions — not bare
except Exception - Fail fast — validate inputs early, raise clear errors
- Don’t swallow exceptions — log and re-raise or handle meaningfully
- Use custom exceptions for domain-specific errors
- Clean up in
finallyor use context managers - Write tests for error paths — they’re often untested
Robust error handling separates production code from scripts that break silently.