Errors should never pass silently.
Unless explicitly silenced.
Tim Peters, Zen of Python
Error handling ensures that your program can respond gracefully to unexpected situations, such as invalid user input, missing files, or network timeouts. In Python, errors are communicated via exceptions. When an error arises, Python creates an exception object, interrupting the normal program flow. By handling exceptions proactively, you can prevent crashes and improve reliability.
Whether you’re developing simple scripts or large-scale applications, robust exception handling is important. It helps you troubleshoot issues faster and provides a better user experience by offering clear error messages or fallback actions instead of abrupt failures.
The try and except keywords are used to catch and
manage exceptions. If an error occurs within the try block, Python checks the
matching except block for that specific exception type, allowing the program
to continue.
try:
# Attempting to divide by zero causes a crash
result = 10 / 0
except ZeroDivisionError:
# This block runs only if the error occurs
print("Error: Cannot divide by zero.")
result = None
print(f"Result: {result}")
In this example, dividing by zero raises a ZeroDivisionError. The
except block catches it, assigns a fallback value, and prevents a crash.
In many languages, the standard approach is "Look Before You Leap" (LBYL)—checking conditions before
performing an action (e.g., if file_exists: open()).
However, Python favors EAFP: "Easier to Ask for Forgiveness than Permission." This means you assume the operation will succeed and capture the error if it fails. This style is often cleaner, faster, and avoids race conditions.
data = {"name": "Alice"}
# LBYL (Look Before You Leap)
if "age" in data:
print(data["age"])
else:
print("Age missing (LBYL)")
# EAFP (Pythonic)
try:
print(data["age"])
except KeyError:
print("Age missing (EAFP)")
A single try block can have multiple except
blocks to handle different types of errors. This lets you tailor your response according to the specific
problem.
It is best practice to catch specific exceptions first (like IndexError) and generic exceptions
(like Exception) last.
try:
my_list = [1, 2, 3]
# Trying to access index 5, which doesn't exist
result = my_list[5]
except IndexError:
result = "Error: List index out of range."
except Exception as e:
# A catch-all for any other unforeseen errors
result = f"Unexpected error: {e}"
print(result)
except: (without specifying an error type). This catches everything,
including system exit signals and keyboard interrupts (Ctrl+C), making it impossible to stop your program.
Always use at least except Exception: if you want to catch generic errors.
Python provides optional else and finally blocks
to further refine error handling:
else executes only if no exception occurs in the
try block. Use this for code that should run only on success.
finally always executes—making it ideal for cleanup tasks (like closing
files or database connections).
try:
result = 10 / 2
except ZeroDivisionError:
print("Divided by zero!")
else:
print(f"Division successful: {result}")
finally:
print("Cleanup: This runs no matter what.")
While try/except handles runtime errors (like missing files), Assertions are
used to catch internal logic errors during development. An assertion checks if a condition is true; if not,
it crashes the program with an AssertionError. This is widely used in Data Science to ensure
data is in the correct shape before processing.
def calculate_discount(price, discount):
# Ensure inputs are logical before proceeding
assert price >= 0, "Price cannot be negative"
assert 0 <= discount <= 1, "Discount must be between 0 and 1"
return price * (1 - discount)
try:
print(f"Price: {calculate_discount(100, 1.5)}")
except AssertionError as e:
print(f"Logic Error: {e}")
You can use the raise statement to manually throw an exception. This
is useful if you want to signal a specific error condition or enforce certain assumptions in your code.
def divide(a, b):
if b == 0:
# We manually raise an error to stop execution
raise ValueError("You can't divide by zero!")
return a / b
try:
result = divide(10, 0)
except ValueError as ve:
result = f"Caught error: {ve}"
print(result)
By raising a ValueError, you clearly highlight the problematic condition. This
approach makes the code’s intent more obvious to anyone reading or using this function.
try-except blocks to catch these specific network errors and implement retry
logic (waiting a few seconds before trying again) rather than crashing the application.
For more complex applications, you may define your own exceptions that extend Python’s built-in
Exception class. Custom exceptions help you handle domain-specific errors
more precisely.
# Define a custom exception
class NegativeValueError(Exception):
pass
def sqrt(value):
if value < 0:
raise NegativeValueError("Cannot compute square root of negative number!")
return value ** 0.5
try:
result = sqrt(-9)
except NegativeValueError as nve:
result = f"Custom Error: {nve}"
print(result)
When exceptions occur, it’s often helpful to log them for troubleshooting or auditing. Python’s
logging module provides a consistent way to track errors.
import logging
# Configure basic logging
logging.basicConfig(level=logging.ERROR)
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
# exc_info=True includes the traceback (stack trace) in the log
logging.error("Attempted to divide by zero.", exc_info=True)
return "undefined"
result = divide(10, 0)
print(f"Result: {result}")
Large Language Models (LLMs) can help you design robust error handling approaches. By outlining the kinds of problems you anticipate—like file system issues, network timeouts, or user input validation—you can request example code that adheres to Python best practices.
Example Prompt:Resulting AI-generated code:
import time
def read_file_with_retry(file_path, retries=3):
for attempt in range(1, retries + 1):
try:
# 'with' automatically closes the file (Cleanup)
with open(file_path, 'r') as file:
return file.read()
except FileNotFoundError:
return "Error: File not found (No retry for missing file)."
except PermissionError:
return "Error: Permission denied."
except Exception as e:
if attempt < retries:
print(f"Attempt {attempt} failed ({e}). Retrying...")
time.sleep(1) # Wait 1 second before retrying
else:
return f"Failed after {retries} attempts. Last error: {e}"
# Example usage with a non-existent file
print(read_file_with_retry("ghost_file.txt"))
You can adapt AI-generated snippets to your specific needs. The Retry Loop shown above is a standard pattern in AI engineering for dealing with flaky network requests.