Python Error Handling & Debugging 2026 — Write Bulletproof Code
Master Python error handling and debugging techniques. Learn try/except, custom exceptions, logging, pdb, and professional debugging strategies to write robust Python code.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Python Error Handling & Debugging 2026 — Write Bulletproof Code
Every programmer spends roughly half their working life debugging. Not because they write bad code — but because software interacts with a chaotic world: networks fail, files are missing, users enter unexpected data, APIs return garbage, and edge cases emerge that no one anticipated.
The difference between a junior and senior Python developer often comes down to one thing: how they handle failure. Juniors hope things work. Seniors plan for things to break.
This guide teaches you to write Python that handles errors gracefully, fails loudly when it should, and is easy to debug when something goes wrong.
Part 1: Understanding Python Exceptions
The Exception Hierarchy
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ValueError
├── TypeError
├── AttributeError
├── NameError
├── IndexError
├── KeyError
├── FileNotFoundError (inherits from OSError)
├── ConnectionError
├── TimeoutError
└── ... (hundreds more)
Exception is the base class for all "normal" exceptions. KeyboardInterrupt and SystemExit inherit from BaseException directly — you almost never want to catch these.
Common Exceptions and What Causes Them
# ValueError — right type, wrong value
int("hello") # ValueError: invalid literal for int()
age = -5
if age < 0: raise ValueError(f"Age cannot be negative: {age}")
# TypeError — wrong type
"hello" + 5 # TypeError: can only concatenate str (not "int") to str
len(42) # TypeError: object of type 'int' has no len()
# KeyError — dictionary key doesn't exist
d = {"a": 1}
d["b"] # KeyError: 'b'
# IndexError — list index out of range
lst = [1, 2, 3]
lst[10] # IndexError: list index out of range
# AttributeError — object doesn't have that attribute
"hello".nonexistent() # AttributeError: 'str' object has no attribute 'nonexistent'
# FileNotFoundError
open("missing.txt") # FileNotFoundError: [Errno 2] No such file or directory
# ZeroDivisionError
10 / 0 # ZeroDivisionError: division by zero
Part 2: try/except — Catching Exceptions
Basic Structure
try:
# Code that might raise an exception
result = int(input("Enter a number: "))
print(f"Result: {100 / result}")
except ValueError:
# Handle invalid conversion
print("Please enter a valid integer")
except ZeroDivisionError:
# Handle division by zero
print("Cannot divide by zero")
except Exception as e:
# Catch any other unexpected exception
print(f"Unexpected error: {type(e).__name__}: {e}")
else:
# Runs only if NO exception was raised
print("Success!")
finally:
# Always runs, whether exception occurred or not
print("Cleaning up...")
Catching Multiple Exceptions Together
try:
data = json.loads(raw_input)
value = data["key"]
except (json.JSONDecodeError, KeyError) as e:
print(f"Data parsing failed: {e}")
The except/else/finally Pattern Explained
def read_config(filepath: str) -> dict:
try:
with open(filepath) as f:
content = f.read()
config = json.loads(content)
except FileNotFoundError:
print(f"Config file not found: {filepath}. Using defaults.")
return {}
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in config file: {e}") from e
else:
print(f"Config loaded successfully: {len(config)} keys")
return config
finally:
print("read_config() completed") # Always runs
Part 3: Creating Custom Exceptions
Custom exceptions make your code expressive and easier to catch at higher levels.
# Define a hierarchy of application-specific exceptions
class AppError(Exception):
"""Base class for all application exceptions."""
pass
class ValidationError(AppError):
"""Raised when user input fails validation."""
def __init__(self, field: str, message: str):
self.field = field
self.message = message
super().__init__(f"Validation error on '{field}': {message}")
class NotFoundError(AppError):
"""Raised when a requested resource doesn't exist."""
def __init__(self, resource: str, identifier):
self.resource = resource
self.identifier = identifier
super().__init__(f"{resource} with id={identifier} not found")
class InsufficientFundsError(AppError):
"""Raised when a transaction cannot be completed due to low balance."""
def __init__(self, balance: float, amount: float):
self.balance = balance
self.amount = amount
super().__init__(
f"Insufficient funds: balance ${balance:.2f}, required ${amount:.2f}"
)
# Using custom exceptions
def withdraw(account_id: int, amount: float) -> float:
if amount <= 0:
raise ValidationError("amount", "Must be positive")
account = get_account(account_id) # Could raise NotFoundError
if account.balance < amount:
raise InsufficientFundsError(account.balance, amount)
account.balance -= amount
return account.balance
# Catching at different levels
try:
new_balance = withdraw(user_account_id, 500)
except ValidationError as e:
return {"error": f"Invalid input: {e.message}", "field": e.field}
except NotFoundError as e:
return {"error": f"Account not found: {e.identifier}"}
except InsufficientFundsError as e:
return {"error": "Insufficient funds", "balance": e.balance, "required": e.amount}
Part 4: Context Managers for Cleanup
The with statement guarantees cleanup even when exceptions occur:
# File handling — always closed even if exception occurs
with open("data.txt") as f:
content = f.read()
# Database connection — always rolled back/closed on exception
from contextlib import contextmanager
@contextmanager
def db_transaction(session):
"""Auto-commit or rollback a database transaction."""
try:
yield session
session.commit()
print("Transaction committed")
except Exception:
session.rollback()
print("Transaction rolled back")
raise # Re-raise the exception after cleanup
with db_transaction(session) as s:
s.add(User(name="Alice"))
s.add(User(name="Bob"))
# Commit happens automatically — or rollback if anything fails
Part 5: Logging — Better Than Print
print() debugging is fine for learning. Production code needs logging.
import logging
# Configure logging format and level
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler(), # Also print to console
]
)
logger = logging.getLogger(__name__)
# Log at different levels
logger.debug("Processing 1,234 records...") # DEBUG: detailed info for development
logger.info("User alice logged in") # INFO: normal operation
logger.warning("API response took 8.3 seconds") # WARNING: something unexpected but ok
logger.error("Failed to send email to alice") # ERROR: something failed
logger.critical("Database connection lost!") # CRITICAL: system is unusable
# Log exceptions with full traceback
try:
result = risky_operation()
except Exception as e:
logger.exception(f"risky_operation failed: {e}") # Logs traceback automatically
Structured Logging for Production
import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
"""Output logs as JSON for easy parsing by log aggregators."""
def format(self, record):
log_data = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"line": record.lineno,
}
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data)
# Use JSON logging in production
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger = logging.getLogger("myapp")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
Part 6: Debugging Techniques
Using pdb — Python's Built-in Debugger
def complex_function(data: list) -> dict:
result = {}
for item in data:
import pdb; pdb.set_trace() # Execution pauses here
# In pdb: n (next), s (step into), c (continue), p variable (print value)
# l (list code), q (quit), h (help)
processed = process(item)
result[item["id"]] = processed
return result
Python 3.7+ shorthand:
def complex_function(data: list) -> dict:
result = {}
for item in data:
breakpoint() # Same as pdb.set_trace()
processed = process(item)
result[item["id"]] = processed
return result
pdb Commands Cheat Sheet
| Command | Action |
|---|---|
n | Execute next line |
s | Step into function call |
c | Continue to next breakpoint |
p expr | Print expression value |
pp expr | Pretty-print expression |
l | Show source code around current line |
w | Show call stack (where am I?) |
u / d | Move up/down the call stack |
b 42 | Set breakpoint at line 42 |
q | Quit debugger |
Debugging with VS Code
For visual debugging, set breakpoints by clicking the gutter in VS Code, then press F5. The debugger shows variables, call stack, and lets you evaluate expressions — much more visual than pdb.
The Art of Defensive Programming
from typing import Optional
def get_user_age(user: Optional[dict]) -> int:
# Check inputs at boundaries — never assume what you receive is valid
if user is None:
raise ValueError("user cannot be None")
if not isinstance(user, dict):
raise TypeError(f"Expected dict, got {type(user).__name__}")
if "age" not in user:
raise KeyError("user dict must contain 'age' key")
age = user["age"]
if not isinstance(age, int) or age < 0:
raise ValueError(f"age must be a non-negative integer, got {age!r}")
return age
# Using assert for internal invariants (disable in production with -O flag)
def calculate_discount(price: float, rate: float) -> float:
assert 0 <= rate <= 1, f"Discount rate must be between 0 and 1, got {rate}"
assert price >= 0, f"Price must be non-negative, got {price}"
return price * (1 - rate)
Part 7: Error Handling Patterns
The Result Pattern (No Exceptions)
from dataclasses import dataclass
from typing import TypeVar, Generic, Optional
T = TypeVar("T")
@dataclass
class Result(Generic[T]):
"""Represent success or failure without raising exceptions."""
value: Optional[T]
error: Optional[str]
success: bool
@classmethod
def ok(cls, value: T) -> "Result[T]":
return cls(value=value, error=None, success=True)
@classmethod
def fail(cls, error: str) -> "Result[T]":
return cls(value=None, error=error, success=False)
def safe_divide(a: float, b: float) -> Result[float]:
if b == 0:
return Result.fail("Division by zero")
return Result.ok(a / b)
result = safe_divide(10, 0)
if result.success:
print(f"Result: {result.value}")
else:
print(f"Error: {result.error}")
Exception Chaining
def parse_config(filepath: str) -> dict:
try:
with open(filepath) as f:
return json.loads(f.read())
except FileNotFoundError as e:
raise ConfigError(f"Cannot load config: {filepath}") from e
# "from e" preserves the original exception as __cause__
# When displayed: "The above exception was the direct cause..."
For putting these skills into practice, the Python testing guide shows you how to write tests that catch these exact types of errors before they reach production. And to see error handling in a full web context, the FastAPI tutorial demonstrates HTTP error handling patterns.
Good error handling is not pessimism — it is professionalism. The code you write today will be debugged by someone at 2 AM when something goes wrong in production. Write it so that person (probably future you) can figure out what happened quickly.
Python debugging cheat sheets and error handling templates available free in the AiTechWorlds Telegram channel!
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
Python Async Programming Guide 2026 — asyncio, aiohttp & Concurrency
Master async programming in Python with asyncio. Learn concurrent programming, aiohttp for async HTTP, async database operations, and build high-performance Python applications.
Python OOP Complete Guide 2026 — Object-Oriented Programming Mastery
Master Python object-oriented programming from basics to advanced. Classes, inheritance, polymorphism, SOLID principles, dataclasses — everything you need to write professional Python.
Python Decorators and Generators — Advanced Python Made Simple 2026
Master Python decorators and generators — two of Python's most powerful features. Clear explanations, real-world examples, and practical patterns you'll actually use.
Python for Machine Learning 2026 — Your First ML Project with scikit-learn
Start your machine learning journey with Python and scikit-learn. Build real ML models, understand the ML workflow, and go from raw data to predictions — complete beginner guide.