Error Handling with try/except
Errors happen in every program. A file might not exist, a network request might time out, or a user might enter invalid input. Without error handling, your program crashes and displays a confusing traceback. With error handling, you can respond gracefully and keep your program running.
Python provides a powerful exception handling mechanism that lets you catch errors and respond to them in a controlled way.
Catching Exceptions with try and except
The try block contains code that might fail. The except block handles the error if one occurs:
try:
with open("config.txt", "r") as file:
data = file.read()
except FileNotFoundError:
print("The configuration file is missing.")
If the file doesn’t exist, Python raises a FileNotFoundError, which the except block catches. Instead of crashing, the program prints a friendly message and continues.
The except block only runs when an exception occurs. If the file exists and reads successfully, the except block is skipped entirely.
How try/except Works
When Python executes a try block:
- The code between
tryand the firstexceptruns normally - If no exception occurs, all
exceptblocks are skipped - If an exception occurs, Python stops executing the
tryblock and looks for a matchingexceptblock - If a matching handler is found, it executes and then continues after the
try/exceptstructure - If no handler matches, the exception propagates up and crashes the program
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("The data file doesn't exist.")
except ValueError:
print("The file doesn't contain a valid number.")
except ZeroDivisionError:
print("Cannot divide by zero.")
Python evaluates the except clauses from top to bottom and executes the first matching one. Order matters—if you put a general exception type before a specific one, the general handler catches everything.
Catching Multiple Exceptions Together
When different exceptions need the same handling, group them in a single except clause:
try:
result = int(user_input) / 0
except (ValueError, ZeroDivisionError):
print("Invalid input or division by zero.")
This is cleaner than writing separate handlers with duplicate code.
Accessing the Exception Object
Sometimes you need details about what went wrong. The as keyword gives you access to the exception instance:
try:
with open("config.json", "r") as file:
config = json.load(file)
except FileNotFoundError as e:
print(f"File missing: {e.filename}")
except json.JSONDecodeError as e:
print(f"Invalid JSON at position {e.pos}: {e.msg}")
Exception objects contain useful attributes. For FileNotFoundError, you can access filename. For JSONDecodeError, you get pos, msg, and lineno.
The else Clause
The else block runs only when the try block completes without raising an exception. 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.")
The else clause separates the success path from error handling. This makes your code easier to read—you know exactly which code runs on success and which runs on failure.
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.")
The finally block is useful for releasing resources, closing database connections, or unlocking files. It runs even if an exception is raised inside the try block.
Using Context Managers Instead
The with statement is a cleaner way to handle file cleanup:
try:
with open("data.txt", "r") as file:
content = file.read()
except FileNotFoundError:
print("File not found.")
Context managers automatically close the file when the block exits, even if an exception occurs. Use with whenever possible—it’s more concise and less error-prone than manual cleanup.
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
Sometimes you want to catch an exception, do something with it (like log it), and then let it continue propagating:
try:
risky_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.
Exception Chaining
You can chain exceptions to show cause and effect:
try:
with open("config.txt", "r") as file:
config = json.load(file)
except FileNotFoundError as exc:
raise RuntimeError("Configuration is required") from exc
The from exc part creates an explicit exception chain. Use from None to suppress the original exception:
raise RuntimeError("Configuration is required") from None
Custom Exceptions
For domain-specific errors, create custom exception classes:
class InvalidAgeError(Exception):
"""Raised when age is not a valid value."""
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}")
Name custom exceptions with “Error” at the end—a convention that makes your code recognizable to other Python developers.
Custom Exceptions with Extra Data
Custom exceptions can carry additional 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 format")
except ValidationError as e:
print(f"Problem in field: {e.field}")
print(f"Details: {e.message}")
Best Practices
Be Specific with Exception Types
Avoid catching all exceptions with a bare except:
# Good - catches specific exception
try:
data = json.loads(user_input)
except json.JSONDecodeError:
print("Invalid JSON")
# Avoid - catches everything including KeyboardInterrupt
try:
data = json.loads(user_input)
except:
print("Invalid JSON")
The bare except also catches KeyboardInterrupt (Ctrl+C) and SystemExit, which users normally expect to terminate the program.
Don’t Use Exceptions for Flow Control
If you can check a condition first, do so instead of using exceptions:
# 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 relatively expensive in Python because building the traceback takes time. Use them for genuinely exceptional situations, not normal control flow.
Use finally for Cleanup
Always clean up resources, even when exceptions occur:
connection = None
try:
connection = connect_to_database()
result = connection.query("SELECT * FROM users")
finally:
if connection:
connection.close()
Or better, use a context manager that handles cleanup automatically.
Common Exception Types
Knowing what exceptions to expect helps you handle them properly:
| Exception | When it occurs |
|---|---|
FileNotFoundError | File doesn’t exist |
PermissionError | No read/write permission |
ValueError | Right type, wrong value |
TypeError | Wrong type entirely |
KeyError | Dictionary key not found |
IndexError | List index out of range |
ZeroDivisionError | Division by zero |
ConnectionError | Network connection failed |
A Practical Example
Here’s a function that parses user input with proper error handling:
def parse_age(input_string):
"""Parse a string into an age integer."""
try:
age = int(input_string.strip())
except ValueError:
raise ValueError("Please enter a valid whole number.")
if age < 0:
raise ValueError("Age cannot be negative.")
if age > 150:
raise ValueError("Please enter a realistic age.")
return age
# Usage
while True:
user_input = input("Enter your age: ")
try:
age = parse_age(user_input)
print(f"You are {age} years old.")
break
except ValueError as e:
print(f"Invalid input: {e}")
This example demonstrates input validation, custom error messages, and looping until valid input is received.
Conclusion
Error handling is essential for writing robust Python programs. The try/except mechanism lets you catch and handle exceptions gracefully. Use else to separate success logic from error handling, and finally for cleanup that must always run. Raise exceptions yourself when something goes wrong, and create custom exception classes for domain-specific errors.
Remember to be specific about which exceptions you catch, avoid using exceptions for normal flow control, and use context managers for resource cleanup. These practices will help you write code that’s easier to maintain and debug.
Next Steps
Now that you understand error handling, explore these related topics:
- The
loggingmodule for recording errors in production - The
tracebackmodule for detailed error analysis - Context managers and the
contextlibmodule - Testing exception handling with pytest