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, orTypeError, rather thanException. Avoid bareexcept: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:
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:
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.
Related Resources
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.
For additional information on related topics, take a look at the following resources:
- Python's raise: Effectively Raising Exceptions in Your Code (Tutorial)
- Python's Built-in Exceptions: A Walkthrough With Examples (Tutorial)
- How to Catch Multiple Exceptions in Python (Tutorial)
- Python 3.11 Preview: Task and Exception Groups (Tutorial)
- Python KeyError Exceptions and How to Handle Them (Tutorial)
- Invalid Syntax in Python: Common Reasons for SyntaxError (Tutorial)
- Understanding the Python Traceback (Tutorial)
- Control Flow Structures in Python (Tutorial)
- Introduction to Python Exceptions (Course)
- Raising and Handling Python Exceptions (Course)
- Python Exceptions: An Introduction (Quiz)
- Using raise for Effective Exceptions (Course)
- Python's raise: Effectively Raising Exceptions in Your Code (Quiz)
- Working With Python's Built-in Exceptions (Course)
- Python's Built-in Exceptions: A Walkthrough With Examples (Quiz)
- KeyError Exceptions in Python and How to Handle Them (Course)
- Identify Invalid Python Syntax (Course)
- Getting the Most Out of a Python Traceback (Course)
- Control Flow Structures in Python (Quiz)