Handling Errors and Exceptions
Errors are a normal part of programming. A file might not exist, a network request might fail, or a user might enter invalid input. Rather than letting your program crash, you handle these situations gracefully with exception handling.
Python’s exception handling mechanism lets you catch errors and respond to them without stopping your program.
Understanding Exceptions
When Python encounters an error, it raises an exception. If that exception is not caught, the program terminates and displays an error message.
# This will crash because the file doesn't exist
with open("nonexistent.txt", "r") as file:
content = file.read()
Running this produces a FileNotFoundError. The traceback shows exactly where the error occurred.
Catching Exceptions with try and except
The try block contains code that might raise an exception. The except block handles it:
try:
with open("nonexistent.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("The file doesn't exist.")
Now the program continues instead of crashing. The user sees a friendly message instead of a traceback.
Handling Multiple Exception Types
A single try block can raise different types of exceptions. Handle each one separately:
try:
with open("data.txt", "r") as file:
number = int(file.read().strip())
result = 100 / number
except FileNotFoundError:
print("File not found.")
except ValueError:
print("The file doesn't contain a valid number.")
except ZeroDivisionError:
print("Cannot divide by zero.")
Python evaluates the except blocks in order and executes the first matching one.
Catching Multiple Exceptions in One Block
If multiple exceptions need the same handling, group them together:
try:
result = int(user_input) / 0
except (ValueError, ZeroDivisionError):
print("Invalid input or division by zero.")
Accessing the Exception Object
Sometimes you need details about the exception. The as keyword gives you access to it:
try:
with open("config.txt", "r") as file:
data = json.load(file)
except FileNotFoundError as e:
print(f"File missing: {e.filename}")
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e.msg} at position {e.pos}")
The exception object contains useful attributes like filename, msg, and pos in this case.
The else Clause
The else block runs only if no exception was raised in the try block. Use it for code that should execute on success:
try:
with open("data.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("File not found.")
else:
print(f"Successfully read {len(content)} characters.")
This separates the “happy path” from error handling, making the code cleaner.
The finally Clause
The finally block runs regardless of whether an exception occurred. Use it for cleanup code that must execute:
file = None
try:
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
print("File not found.")
finally:
if file:
file.close()
print("Cleanup complete.")
A better approach uses the with statement, which handles closing automatically:
try:
with open("data.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("File not found.")
The with statement is a context manager that guarantees cleanup. Use it for file operations and other resources.
Raising Exceptions
You can raise exceptions yourself using the raise keyword. This is useful for validating input or signaling errors:
def divide(a, b):
if b == 0:
raise ValueError("Divisor cannot be zero.")
return a / b
try:
result = divide(10, 0)
except ValueError as e:
print(f"Error: {e}")
Re-raising Exceptions
Inside an except block, you might want to log the error but still let it propagate:
try:
dangerous_operation()
except Exception as e:
print(f"Something went wrong: {e}")
raise # Re-raises the same exception
The bare raise statement re-raises the caught exception.
Raising a Different Exception
Sometimes you want to catch an exception and raise a different one:
try:
with open("config.txt", "r") as file:
config = json.load(file)
except FileNotFoundError:
raise RuntimeError("Configuration file is missing.") from None
The from None suppresses the original exception chain. Use from original_exception to chain them.
Custom Exceptions
When your code has specific error conditions, create custom exception classes:
class InvalidAgeError(Exception):
"""Raised when age is not a positive integer."""
pass
def set_age(age):
if not isinstance(age, int):
raise InvalidAgeError("Age must be an integer.")
if age < 0:
raise InvalidAgeError("Age cannot be negative.")
return age
try:
set_age(-5)
except InvalidAgeError as e:
print(f"Invalid age: {e}")
Naming your exception with “Error” at the end follows Python conventions. Inherit from Exception directly for simple cases.
Custom Exceptions with Additional Data
Custom exceptions can carry extra information:
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
try:
raise ValidationError("email", "Invalid email format")
except ValidationError as e:
print(f"Field: {e.field}, Error: {e.message}")
Exception Hierarchy
Python has a built-in exception hierarchy. Knowing it helps you catch the right exceptions:
├── SystemExit
├── KeyboardInterrupt
└── Exception
├── StopIteration
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError (IOError)
│ ├── FileNotFoundError
│ └── PermissionError
├── ValueError
├── TypeError
└── ...
Catching a parent exception also catches its children. Be specific when you can:
# This catches ALL exceptions, including KeyboardInterrupt and SystemExit
# Don't do this unless you really mean it
try:
risky_operation()
except Exception:
print("Something went wrong")
Best Practices
Be Specific in except Blocks
Catch specific exceptions rather than using a bare except:
# Good
try:
data = json.loads(user_input)
except json.JSONDecodeError:
print("Invalid JSON")
# Avoid
try:
data = json.loads(user_input)
except:
print("Invalid JSON")
The bare except also catches KeyboardInterrupt and SystemExit, which usually shouldn’t be caught.
Use Exceptions for Exceptional Cases
Don’t use exceptions for flow control. If you can check a condition first, do so:
# Better - check first
if key in my_dict:
value = my_dict[key]
else:
value = default
# Works but less efficient
try:
value = my_dict[key]
except KeyError:
value = default
Exceptions are slow in Python because they involve creating objects and building tracebacks.
Clean Up with finally or Context Managers
Always release resources:
# Using finally
connection = None
try:
connection = connect_to_server()
data = connection.fetch()
finally:
if connection:
connection.close()
# Using context manager (preferred)
with connect_to_server() as connection:
data = connection.fetch()
Example: A Robust Configuration Loader
Putting it all together, here’s a function that loads configuration with proper error handling:
import json
from pathlib import Path
class ConfigError(Exception):
"""Base exception for configuration errors."""
pass
class ConfigNotFoundError(ConfigError):
pass
class ConfigValidationError(ConfigError):
pass
def load_config(path):
"""Load configuration from a JSON file."""
# Check if file exists
if not Path(path).exists():
raise ConfigNotFoundError(f"Config file not found: {path}")
try:
with open(path, "r") as f:
config = json.load(f)
except json.JSONDecodeError as e:
raise ConfigValidationError(f"Invalid JSON: {e}")
# Validate required fields
required = ["name", "version"]
for field in required:
if field not in config:
raise ConfigValidationError(f"Missing required field: {field}")
return config
# Usage
try:
config = load_config("app_config.json")
print(f"Loaded config: {config['name']} v{config['version']}")
except ConfigNotFoundError as e:
print(f"Setup needed: {e}")
except ConfigValidationError as e:
print(f"Config error: {e}")
except ConfigError as e:
print(f"Unexpected config error: {e}")
This demonstrates custom exceptions, proper error hierarchy, validation, and clean error messages.
Next Steps
You now know how to handle errors gracefully in Python. These skills help you write programs that don’t crash unexpectedly and provide useful feedback to users.
To continue learning, explore these topics:
- The
tracebackmodule for detailed error information - The
loggingmodule for recording errors in production - Context managers and the
contextlibmodule - Testing exception handling with pytest
The next tutorial covers list, dict, and set comprehensions, which are powerful ways to create and transform collections in Python.