Error Handling
Gracefully handling errors and exceptions with try/except blocks
Introduction
Errors are inevitable in programming. Whether it's invalid user input, missing files, network issues, or unexpected data, your programs need to handle errors gracefully. Python uses exceptions to signal errors, and try/except blocks allow you to catch and handle these exceptions. Proper error handling makes your programs more robust, user-friendly, and easier to debug.
What are Exceptions?
An exception is an event that occurs during program execution that disrupts the normal flow. When Python encounters an error, it raises an exception. If not handled, the program crashes.
Common Errors Without Handling
# Division by zero
result = 10 / 0
# ZeroDivisionError: division by zero
# Invalid type conversion
number = int("abc")
# ValueError: invalid literal for int() with base 10: 'abc'
# Accessing non-existent key
person = {"name": "Alice"}
age = person["age"]
# KeyError: 'age'
# Accessing list index out of range
numbers = [1, 2, 3]
value = numbers[10]
# IndexError: list index out of range
# Calling method on None
text = None
length = text.upper()
# AttributeError: 'NoneType' object has no attribute 'upper'Without error handling: These errors cause your program to crash immediately, showing a traceback to the user, which is not a good user experience!
Basic Try/Except Blocks
The try/except block allows you to catch and handle exceptions gracefully.
Basic Syntax
try:
# Code that might raise an exception
result = 10 / 0
except:
# Code to handle the exception
print("An error occurred!")
# The program continues after the except block
print("Program continues...")Practical Example: Division
def divide(a, b):
try:
result = a / b
return result
except:
print("Error: Cannot divide by zero!")
return None
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # Error: Cannot divide by zero!
# None
# Program doesn't crash!Catching Specific Exceptions
It's better to catch specific exceptions rather than all exceptions:
try:
number = int(input("Enter a number: "))
result = 10 / number
print(f"Result: {result}")
except ZeroDivisionError:
print("Error: Cannot divide by zero!")
except ValueError:
print("Error: Invalid number entered!")
except Exception as e:
print(f"An unexpected error occurred: {e}")Best Practice: Always catch specific exceptions when possible. Catching all exceptions with bare except: can hide bugs and make debugging harder. Important: There are exceptions to this rule. For example, in ETL (Extract-Transform-Load) pipelines or data processing jobs, you may want your code to continue running no matter what happens, deferring error handling, postmortem analysis, or retries to other specialized tools or workflows outside your code. In such cases, broadly catching exceptions may be appropriate.
Multiple Except Clauses
You can handle multiple different exceptions with separate except clauses.
Handling Different Exceptions
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Error: Division by zero!")
return None
except TypeError:
print("Error: Invalid types for division!")
return None
except Exception as e:
print(f"Unexpected error: {e}")
return None
# Test different scenarios
safe_divide(10, 2) # 5.0
safe_divide(10, 0) # Error: Division by zero!
safe_divide("10", 2) # Error: Invalid types for division!Catching Multiple Exceptions
You can catch multiple exception types in a single except clause:
try:
# Some code that might raise exceptions
value = int(input("Enter a number: "))
result = 100 / value
print(f"Result: {result}")
except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")
except ZeroDivisionError:
print("Cannot divide by zero!")
except Exception as e:
print(f"Unexpected error: {e}")Else and Finally Clauses
The else clause runs when no exception occurs, and finally always runs, regardless of exceptions.
The Else Clause
try:
number = int(input("Enter a number: "))
result = 10 / number
except ZeroDivisionError:
print("Cannot divide by zero!")
except ValueError:
print("Invalid number!")
else:
# Runs only if no exception occurred
print(f"Success! Result: {result}")
# The else block is useful for code that should only
# run when the try block succeeds completelyThe Finally Clause
The finally block always executes, whether an exception occurs or not. It's perfect for cleanup code:
try:
file = open("data.txt", "r")
content = file.read()
print(content)
except FileNotFoundError:
print("File not found!")
except Exception as e:
print(f"Error: {e}")
finally:
# Always runs, even if exception occurs
print("Cleaning up...")
# Close file, release resources, etc.
# In real code, you'd use: file.close() if 'file' in locals() else None
# Example: Ensuring cleanup happens
def process_data():
resource = acquire_resource()
try:
# Use resource
result = use_resource(resource)
return result
finally:
# Always release resource, even if error occurs
release_resource(resource)Complete Try/Except/Else/Finally
def divide_numbers(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: Division by zero!")
return None
except TypeError:
print("Error: Invalid types!")
return None
else:
print("Division successful!")
return result
finally:
print("This always runs!")
# Test
divide_numbers(10, 2)
# Output:
# Division successful!
# This always runs!
# Returns: 5.0
divide_numbers(10, 0)
# Output:
# Error: Division by zero!
# This always runs!
# Returns: NoneCommon Exception Types
Python has many built-in exception types. Here are the most common ones you'll encounter:
ValueError
Raised when a function receives an argument of correct type but inappropriate value.
int("abc")
# ValueError: invalid literalTypeError
Raised when an operation is performed on an object of inappropriate type.
"5" + 3 # TypeError: can only concatenate
KeyError
Raised when a dictionary key is not found.
d = {"a": 1}
d["b"]
# KeyError: 'b'IndexError
Raised when a sequence subscript is out of range.
lst = [1, 2, 3] lst[10] # IndexError: list index out of range
ZeroDivisionError
Raised when division or modulo by zero occurs.
10 / 0 # ZeroDivisionError: division by zero
FileNotFoundError
Raised when a file or directory is requested but doesn't exist.
open("missing.txt")
# FileNotFoundError: [Errno 2]Raising Exceptions
You can raise exceptions yourself using the raise statement. This is useful for validating input or signaling errors in your functions.
Raising Built-in Exceptions
def calculate_age(birth_year):
if birth_year < 0:
raise ValueError("Birth year cannot be negative")
if birth_year > 2024:
raise ValueError("Birth year cannot be in the future")
age = 2024 - birth_year
return age
# Usage
try:
age = calculate_age(1990)
print(f"Age: {age}")
except ValueError as e:
print(f"Error: {e}")
# Test with invalid input
try:
age = calculate_age(-5)
except ValueError as e:
print(f"Error: {e}") # Error: Birth year cannot be negativeRaising with Custom Messages
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > balance:
raise ValueError(f"Insufficient funds. Balance: {balance}")
return balance - amount
# Usage
account_balance = 100
try:
new_balance = withdraw(account_balance, 50)
print(f"New balance: {new_balance}")
except ValueError as e:
print(f"Transaction failed: {e}")
try:
new_balance = withdraw(account_balance, 150)
except ValueError as e:
print(f"Transaction failed: {e}") # Transaction failed: Insufficient fundsRe-raising Exceptions
You can catch an exception, do something, then re-raise it:
def process_data(data):
try:
result = int(data) * 2
return result
except ValueError:
print("Logging error...")
raise # Re-raise the same exception
# The exception propagates up
try:
process_data("abc")
except ValueError as e:
print(f"Caught in outer handler: {e}")Chaining Exceptions with raise ... from ...
Sometimes you want to raise a new exception, but keep information about the original exception. Python allows you to chain exceptions using raise ... from .... This makes debugging easier by preserving the entire error chain.
def parse_age(age_str):
try:
age = int(age_str)
if age < 0:
raise ValueError("Age cannot be negative")
return age
except ValueError as e:
raise RuntimeError("Failed to parse age") from e # Chain the original error
try:
user_age = parse_age("not_a_number")
except RuntimeError as err:
print(f"Error: {err}")
print(f"Original cause: {err.__cause__}") # Access the original exception
# This prints both errors and preserves traceback context
Practical Examples
Example: Safe Input Validation
def get_positive_number(prompt):
"""Get a positive number from user with error handling."""
while True:
try:
number = float(input(prompt))
if number <= 0:
raise ValueError("Number must be positive")
return number
except ValueError as e:
print(f"Invalid input: {e}")
print("Please try again.")
# Usage
age = get_positive_number("Enter your age: ")
print(f"You entered: {age}")
# Keeps asking until valid input is providedExample: Safe Dictionary Access
def safe_get_value(data, key, default=None):
"""Safely get value from dictionary."""
try:
return data[key]
except KeyError:
return default
except TypeError:
print("Error: First argument must be a dictionary")
return default
# Usage
person = {"name": "Alice", "age": 30}
name = safe_get_value(person, "name", "Unknown")
email = safe_get_value(person, "email", "No email")
print(name) # Alice
print(email) # No email
# Alternative: Use .get() method (built-in)
name = person.get("name", "Unknown")
email = person.get("email", "No email")Example: File Operations
def read_file_safely(filename):
"""Read file content with proper error handling."""
try:
with open(filename, 'r') as file:
content = file.read()
return content
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
return None
except PermissionError:
print(f"Error: Permission denied to read '{filename}'.")
return None
except Exception as e:
print(f"Unexpected error: {e}")
return None
# Usage
content = read_file_safely("data.txt")
if content:
print("File read successfully!")
print(content)
# Note: Using 'with' statement automatically closes file
# even if an exception occurs (better than try/finally)Best Practices
✓ Do
- Catch specific exceptions
- Provide meaningful error messages
- Use
finallyfor cleanup - Log errors for debugging
- Handle errors at appropriate level
- Use
withfor file operations
✗ Don't
- Catch all exceptions with bare
except: - Silently ignore errors
- Use exceptions for control flow
- Catch exceptions you can't handle
- Leave resources open
- Hide error details from users
Key Takeaways
Try/Except Basics
- Use
try/exceptto handle errors - Catch specific exceptions when possible
- Provide clear error messages
- Let program continue after handling
Advanced Features
elseruns when no exceptionfinallyalways runs- Use
raiseto signal errors - Re-raise exceptions when needed
Common Exceptions
ValueError,TypeErrorKeyError,IndexErrorZeroDivisionErrorFileNotFoundError
Best Practices
- Be specific with exception types
- Use
withfor file operations - Don't use exceptions for control flow
- Always clean up resources
What's Next?
Excellent! You now know how to handle errors gracefully. Your programs will be much more robust and user-friendly. In future lessons, we'll explore:
- Working with files - Read from and write to files safely using context managers
- Modules and imports - Organize your code across multiple files
- String manipulation - Advanced string operations and formatting
- Standard library tools - Leverage Python's powerful built-in modules