Introduction to error handling (also called exception handling) is how Python programs gracefully detect, respond to, and recover from runtime errors instead of crashing. Errors — like dividing by zero, accessing missing keys, file not found, or invalid input — are represented as exceptions, special objects that carry information about what went wrong. Mastering error handling makes your code robust, user-friendly, and production-ready.
In 2026, good Python code uses structured exception handling with try/except/else/finally, custom exceptions, logging, and context managers. Here’s a complete, practical guide to handling errors effectively — with real patterns, best practices, and modern tips.
Start with the basics: wrap risky code in a try block, catch specific exceptions in except, run success code in else, and always-cleanup code in finally.
try:
result = 1 / 0 # This will raise ZeroDivisionError
except ZeroDivisionError as e:
print(f"Error: {e}") # Handle specific error
else:
print("Success! Result is:", result) # Only runs if no exception
finally:
print("Cleanup: This always runs") # Runs no matter what
# Output:
# Error: division by zero
# Cleanup: This always runs
Catch multiple exceptions, use the exception object for details, and avoid bare except: — it hides bugs.
try:
num = int(input("Enter a number: "))
result = 100 / num
except ValueError:
print("Invalid input — please enter a number")
except ZeroDivisionError:
print("Cannot divide by zero")
except Exception as e: # Catch-all (use sparingly)
print(f"Unexpected error: {type(e).__name__} - {e}")
else:
print(f"Result: {result}")
finally:
print("Thanks for trying!")
Raise your own exceptions with raise — great for validation or custom errors.
def divide_safe(x: float, y: float) -> float:
"""Divide x by y — raise custom error on zero."""
if y == 0:
raise ValueError("Division by zero is not allowed")
return x / y
try:
print(divide_safe(10, 0))
except ValueError as e:
print(f"Custom error: {e}")
Custom exceptions make errors meaningful and catchable — inherit from built-in Exception or a specific base.
class InvalidAgeError(Exception):
"""Custom exception for invalid age values."""
pass
def set_age(age: int) -> None:
if age < 0 or age > 150:
raise InvalidAgeError(f"Age must be between 0 and 150, got {age}")
print(f"Age set to {age}")
try:
set_age(-5)
except InvalidAgeError as e:
print(f"Validation failed: {e}")
Context managers (with) handle cleanup automatically — perfect for files, database connections, locks.
with open("data.txt", "r") as f:
content = f.read()
# File auto-closes even if exception occurs
# Or custom context manager
from contextlib import contextmanager
@contextmanager
def temp_value():
print("Setup")
try:
yield "temporary value"
finally:
print("Cleanup")
with temp_value() as val:
print(val) # Setup ? temporary value ? Cleanup
Best practices make error handling reliable and maintainable. Catch specific exceptions — never bare except: — it swallows bugs silently. Use as e to access exception details. Prefer else for success code — keeps logic clean. Always use finally for cleanup (files, connections). Log exceptions — use logging instead of print in production. Raise meaningful custom exceptions — inherit from built-ins or create hierarchies. Pitfalls include catching too broadly (hides errors), not re-raising in except when needed, and ignoring cleanup. Modern tip: use structural pattern matching (Python 3.10+) in except for advanced handling, and contextlib.suppress to silently ignore specific errors.
Error handling turns crashes into controlled responses. In 2026, use try/except/else/finally, raise custom exceptions, leverage context managers, and log properly. Master it, and your code becomes robust, debuggable, and user-friendly — ready for real-world use.
Next time something can fail — wrap it in try, catch what you expect, clean up in finally, and log the rest. It’s not just error handling — it’s professional Python.