Good error handling makes code robust and debuggable. Here's how to do it right in Python.
Basic try/except
try:
result = risky_operation()
except SomeError:
handle_error()Catching Specific Exceptions
Always catch specific exceptions:
# Bad - catches everything including KeyboardInterrupt
try:
data = json.loads(text)
except:
pass
# Better - catches only what you expect
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}")Multiple Exceptions
Handle different errors differently:
try:
with open(path) as f:
data = json.load(f)
except FileNotFoundError:
data = {} # Use default
except json.JSONDecodeError:
raise ValueError(f"Invalid JSON in {path}")
except PermissionError:
raise # Re-raise as-isOr catch multiple as one:
try:
process(data)
except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")The else Clause
Runs only if no exception occurred:
try:
result = fetch_data()
except NetworkError:
result = None
else:
# Only runs if fetch succeeded
save_to_cache(result)The finally Clause
Always runs, even if there's an exception:
try:
conn = open_connection()
result = conn.query(sql)
except DatabaseError:
log_error()
raise
finally:
conn.close() # Always closeRe-raising Exceptions
Keep the original traceback:
try:
process()
except ValueError:
log("Processing failed")
raise # Re-raises with original tracebackException Chaining
Show the cause of an error:
try:
data = fetch_from_api()
except RequestError as e:
raise DataError("Failed to get data") from e
# Traceback shows both exceptionsCustom Exceptions
Create meaningful exceptions for your domain:
class AppError(Exception):
"""Base exception for our application."""
pass
class ValidationError(AppError):
"""Input validation failed."""
pass
class NotFoundError(AppError):
"""Resource not found."""
pass
class AuthenticationError(AppError):
"""Authentication failed."""
passWith extra context:
class APIError(Exception):
def __init__(self, message, status_code=None, response=None):
super().__init__(message)
self.status_code = status_code
self.response = response
# Usage
raise APIError("Request failed", status_code=500, response=resp)
# Handling
except APIError as e:
if e.status_code == 429:
wait_and_retry()Exception Hierarchies
Design exceptions for flexible catching:
class StorageError(Exception):
"""Base for storage errors."""
pass
class ConnectionError(StorageError):
"""Can't connect to storage."""
pass
class ReadError(StorageError):
"""Can't read from storage."""
pass
class WriteError(StorageError):
"""Can't write to storage."""
pass
# Users can catch specific or broad
try:
save_data()
except WriteError:
# Handle writes specifically
except StorageError:
# Or catch all storage issuesEAFP vs LBYL
EAFP (Easier to Ask Forgiveness than Permission):
# Pythonic
try:
value = dictionary[key]
except KeyError:
value = defaultLBYL (Look Before You Leap):
# Less Pythonic
if key in dictionary:
value = dictionary[key]
else:
value = defaultEAFP is usually preferred in Python. But use LBYL for expensive checks or when exceptions have side effects.
Context Managers for Cleanup
Better than try/finally:
# Instead of try/finally
try:
f = open("file.txt")
data = f.read()
finally:
f.close()
# Use context manager
with open("file.txt") as f:
data = f.read()Logging Exceptions
Log with full traceback:
import logging
try:
process()
except Exception:
logging.exception("Processing failed") # Includes traceback
raiseSuppressing Exceptions
When you really want to ignore errors:
from contextlib import suppress
# Instead of try/except/pass
with suppress(FileNotFoundError):
os.remove("temp.txt")Anti-Patterns
Don't catch Exception blindly:
# Bad
try:
do_something()
except Exception:
pass # Swallows all errorsDon't use exceptions for flow control:
# Bad
try:
items[index]
except IndexError:
return None
# Better
if index < len(items):
return items[index]
return NoneDon't ignore exceptions silently:
# Bad
except SomeError:
pass
# Better - at least log
except SomeError:
logger.warning("Operation failed, continuing")Quick Reference
try:
risky_code()
except SpecificError as e:
handle_specific(e)
except (Error1, Error2):
handle_multiple()
except Exception as e:
log_unexpected(e)
raise
else:
on_success()
finally:
cleanup()Good error handling is about being intentional. Catch what you expect, let unexpected errors propagate, and always preserve context for debugging.