Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →

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.

A
AiTechWorlds Team
May 10, 2026 8 min readUpdated May 15, 2026
📱

Get more content like this on Telegram!

Daily AI tips, notes & resources — free

Join 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

CommandAction
nExecute next line
sStep into function call
cContinue to next breakpoint
p exprPrint expression value
pp exprPretty-print expression
lShow source code around current line
wShow call stack (where am I?)
u / dMove up/down the call stack
b 42Set breakpoint at line 42
qQuit 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!

Share this article:

Frequently Asked Questions

In Python, both are exceptions. SyntaxError and IndentationError occur before code runs. Runtime exceptions (like ValueError, TypeError, FileNotFoundError) occur during execution and can be caught with try/except.
A

AiTechWorlds Team

✓ Verified Writer

The 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

10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!