Exception Handling

Exceptions are the Python way to report and handle errors. Instead of letting your program crash with a confusing traceback, you can detect problems, raise meaningful exceptions, and handle them at the right layer of your application.

Good exception handling makes your code fail fast, issue clear error messages, and recover gracefully when appropriate. Poor exception handling can hide bugs and make your code difficult to debug and maintain.

For exceptions and error handling, the following best practices lead to clearer failures and easier debugging:

  • Fail fast and provide clear error messages. Raise exceptions as soon as something goes wrong, rather than letting invalid state propagate through your code. This practice keeps failures closer to their root causes.
  • Raise low, catch high. Let lower-level functions raise exceptions and catch them at the edges of your program, such as a CLI command, web handler, or GUI event loop. This separation keeps core logic clean while allowing front-ends to decide how to report errors to users.
  • Embrace the EAFP style for risky operations. Favor the easier to ask forgiveness than permission (EAFP) pattern over the look before you leap (LBYL) one. Try an operation and handle the exception if it fails, rather than writing extensive precondition checks.
  • Catch the narrowest exception you can handle. Use specific exceptions, such as FileNotFoundError, ValueError, or TypeError, rather than Exception. Avoid bare except: clauses. This practice prevents masking unrelated bugs.
  • Prefer built-in exceptions and use custom ones for domain-specific errors. Before creating a custom exception, check whether a built-in exception applies to your case. When you need a domain-specific error, define a small hierarchy of custom exceptions that inherit from Exception.
  • Avoid using exceptions for routine application-level control flow. Exceptions should signal error conditions, not replace ordinary branching logic. While Python internally relies on exceptions in some patterns, using them for regular program flow usually harms readability.
  • Log exceptions with useful context. Log errors with enough information to debug them later, and present users with clear, friendly messages.

To see these ideas in practice, consider a small configuration loader:

🔴 Avoid this:

Python
import json

def load_config(path):
    try:
        with open(path, encoding="utf-8") as config:
            data = json.load(config)
    except Exception:
        # If something went wrong, return an empty config
        return {}
    return data

def main():
    try:
        config = load_config("settings.json")
    except Exception:
        print("Sorry, something went wrong.")
    else:
        # Do something with config...

This code works, but the low-level load_config() function catches every exception that can occur while loading the configuration file and silently falls back to an empty dictionary. In main(), the code catches the broad Exception and discards context that would be essential for debugging.

Favor this:

Python
import json
import logging

log = logging.getLogger(__name__)

class ConfigError(Exception):
    """Raised when issues occur with the config file."""

def load_config(path):
    try:
        with open(path, encoding="utf-8") as config:
            data = json.load(config)
    except FileNotFoundError as error:
        raise ConfigError(f"Config file not found: {path}") from error
    except json.JSONDecodeError as error:
        raise ConfigError(f"Invalid JSON: {path}") from error
    return data

def main():
    try:
        config = load_config("settings.json")
    except ConfigError:
        log.exception("Error loading the config")
        print("Sorry, something went wrong while loading the settings.")
    else:
        # Do something with config...

In this version, the load_config() function fails fast by raising a domain-specific ConfigError with clear messages tied to the underlying failure.

The function catches only the specific exceptions associated with I/O or JSON parsing issues. It also uses the from error specifier to preserve the original traceback, which provides valuable debugging context.

In main(), you handle the exception at the application boundary. Technical details—including the traceback—are logged, while the user sees a consistent, friendly message printed on the screen.

Tutorial

Python Exceptions: An Introduction

In this beginner tutorial, you'll learn what exceptions are good for in Python. You'll see how to raise exceptions and how to handle them with try ... except blocks.

basics python

For additional information on related topics, take a look at the following resources:


By Leodanis Pozo Ramos • Updated Dec. 23, 2025 • Reviewed by Brenda Weleschuk and Bartosz Zaczyński