Python Error Handling Cheat Sheet
Quick-reference for exception handling patterns. Each section includes copy-ready snippets with inline output comments.
try / except / else / finally
The full four-part structure: try (risky code), except (error recovery), else (success-only), finally (always runs).
try:
result = int(user_input) # risky code
except ValueError as e:
print(f'Invalid: {e}') # recovery
result = 0
else:
print(f'Parsed: {result}') # only if no exception
finally:
print('Done') # always runstry:
value = int('abc')
except ValueError:
value = 0
value # => 0# else keeps the try block minimal.
# Exceptions in else are NOT caught by the preceding except.
try:
data = json.load(f)
except json.JSONDecodeError:
data = {}
else:
process(data) # error here propagates normallyCatching Specific Exceptions
Always catch the most specific exception possible. Bare except: and except Exception are usually too broad.
try:
result = operation()
except (ValueError, TypeError) as e:
print(f'Error: {e}')try:
with open('config.json') as f:
config = json.load(f)
except FileNotFoundError:
config = {} # missing file is OK
except json.JSONDecodeError as e:
print(f'Malformed JSON: {e}')
config = {}# ValueError — wrong value ('abc' to int)
# TypeError — wrong type (1 + 'a')
# KeyError — missing dict key
# IndexError — list index out of range
# FileNotFoundError — file does not exist
# AttributeError — missing attribute
# ZeroDivisionError — division by zero# except Exception: catches normal errors (recommended)
# except BaseException: also catches KeyboardInterrupt, SystemExit
# except: same as BaseException (avoid!)Never use bare except: — it catches Ctrl+C and prevents clean shutdown.
raise and re-raise
def validate_age(age):
if age < 0:
raise ValueError(f'Age cannot be negative: {age}')
return agetry:
process(data)
except Exception:
logging.exception('Processing failed')
raise # re-raise the same exceptionUse bare raise (no argument) to preserve the original traceback.
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigError('Invalid config') from e
# Traceback shows: "The above exception was the direct cause..."raise AppError('...') from None
# Hides the original exception (use sparingly)Custom Exception Classes
Create domain-specific exceptions by inheriting from Exception. Use a base class for your app.
class ConfigError(Exception):
"""Raised when configuration is invalid."""
pass
raise ConfigError('Missing required field: name')class AppError(Exception):
"""Base for all app errors."""
pass
class NotFoundError(AppError):
pass
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
super().__init__(f'{field}: {message}')try:
load_config()
except AppError as e:
# Catches NotFoundError, ValidationError, etc.
print(f'App error: {e}')Exception Chaining (raise ... from ...)
Preserve the original exception context when translating errors between layers.
try:
user = db.get_user(user_id)
except DatabaseError as e:
raise UserNotFoundError(f'User {user_id}') from e
# Traceback shows both exceptions with clear causal linktry:
user = db.get_user(user_id)
except DatabaseError:
raise UserNotFoundError(f'User {user_id}')
# Still chains via __context__, but message says:
# "During handling of the above exception, another exception occurred"Use from for deliberate translation. Without from, Python chains implicitly but the intent is unclear.
try:
load_config()
except ConfigError as e:
print(e.__cause__) # explicit chain (from)
print(e.__context__) # implicit chainContext Managers (__enter__ / __exit__)
The with statement ensures cleanup runs even on exceptions. Prefer it over try/finally.
with open('data.txt') as f:
data = f.read()
# f.close() called automatically
import threading
lock = threading.Lock()
with lock:
# critical section
passclass Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f'Elapsed: {self.elapsed:.4f}s')
return False # don't suppress exceptions
with Timer() as t:
do_work()from contextlib import contextmanager
@contextmanager
def timer():
start = time.perf_counter()
yield
print(f'Elapsed: {time.perf_counter() - start:.4f}s')
with timer():
do_work()assert Statements
assert is for debugging invariants, not for input validation. Assertions are stripped when running with python -O.
def process(items):
assert len(items) > 0, 'items must not be empty'
# ...# Good: internal invariants / developer mistakes
assert isinstance(result, dict), f'Expected dict, got {type(result)}'
# Bad: user input validation (stripped with -O!)
# assert user_age > 0 # Don't do this!
if user_age <= 0:
raise ValueError('Age must be positive')# assert condition, "message"
# Raises AssertionError if condition is False
assert 0 < x < 100, f'x out of range: {x}'Never use assert for security or input validation. Use raise ValueError instead.
Common Built-in Exceptions
int('abc') # ValueError
1 + 'a' # TypeError
len(42) # TypeErrord = {'a': 1}
d['z'] # KeyError: 'z'
items = [1, 2]
items[5] # IndexError: list index out of range'hello'.foo # AttributeError
print(undefined) # NameErroropen('missing.txt') # FileNotFoundError
open('/etc/shadow') # PermissionError
1 / 0 # ZeroDivisionError# BaseException
# +-- SystemExit
# +-- KeyboardInterrupt
# +-- Exception
# +-- ValueError
# +-- TypeError
# +-- LookupError
# | +-- KeyError
# | +-- IndexError
# +-- OSError
# | +-- FileNotFoundError
# | +-- PermissionError
# +-- RuntimeError
# +-- StopIterationCan you write this from memory?
Write a try/except skeleton that catches Exception. Use pass in both blocks.