Exception Handling in Python
Exception handling is a crucial aspect of writing robust Python code. Exceptions are events that occur during program execution that disrupt the normal flow. Instead of letting these events crash your program, Python allows you to handle them gracefully, making your code more resilient and user-friendly.
This guide covers the fundamentals of Python's exception handling system, including built-in exceptions, handling techniques, custom exceptions, and best practices.
Understanding Exceptions
What Are Exceptions?
Exceptions are objects that represent errors or unexpected conditions in your code. When an exception occurs, Python creates an exception object and stops normal execution, looking for exception handling code that can deal with it.
# Common exceptions in Python # 1. SyntaxError - Invalid Python syntax # if True print("Hello") # SyntaxError: invalid syntax # 2. TypeError - Operation/function applied to inappropriate type # "2" + 2 # TypeError: can only concatenate str (not "int") to str # 3. NameError - Name not defined # print(undefined_variable) # NameError: name 'undefined_variable' is not defined # 4. IndexError - Index out of range # lst = [1, 2, 3] # print(lst[5]) # IndexError: list index out of range # 5. KeyError - Key not found in dictionary # d = {"a": 1} # print(d["b"]) # KeyError: 'b' # 6. ValueError - Value inappropriate for operation # int("abc") # ValueError: invalid literal for int() with base 10: 'abc' # 7. AttributeError - Object has no attribute/method # "hello".append("world") # AttributeError: 'str' object has no attribute 'append' # 8. ZeroDivisionError - Division by zero # 10 / 0 # ZeroDivisionError: division by zero # 9. FileNotFoundError - File doesn't exist # open("nonexistent_file.txt") # FileNotFoundError # 10. ImportError - Module not found # import nonexistent_module # ImportError: No module named 'nonexistent_module'
Exception Hierarchy
Python exceptions are organized in a hierarchy. All exceptions derive from the BaseException
class.
# Partial hierarchy of built-in exceptions # BaseException # ├── SystemExit # Raised by sys.exit() # ├── KeyboardInterrupt # Raised when the user presses Ctrl+C # ├── GeneratorExit # Raised when a generator is closed # └── Exception # Base class for most exceptions # ├── StopIteration # Raised when iterator has no more items # ├── ArithmeticError # Base for arithmetic errors # │ ├── FloatingPointError # │ ├── OverflowError # │ └── ZeroDivisionError # ├── AttributeError # ├── ImportError # │ └── ModuleNotFoundError # ├── LookupError # Base for lookup errors # │ ├── IndexError # │ └── KeyError # ├── NameError # ├── SyntaxError # ├── TypeError # ├── ValueError # └── ... many more
Basic Exception Handling
Try-Except Blocks
The basic mechanism for handling exceptions in Python is the try-except block.
# Basic try-except block try: # Code that might raise an exception result = 10 / 0 except ZeroDivisionError: # Code to handle the exception print("Cannot divide by zero!") # Handling multiple exceptions try: number = int(input("Enter a number: ")) result = 10 / number except ValueError: print("That's not a valid number!") except ZeroDivisionError: print("Cannot divide by zero!") # Handling multiple exceptions with the same handler try: # Some code that might raise different exceptions with open("file.txt") as f: content = f.read() number = int(content) except (FileNotFoundError, ValueError) as e: print(f"An error occurred: {e}") # Using the exception object try: numbers = [1, 2, 3] print(numbers[5]) except IndexError as e: print(f"Error: {e}") # Error: list index out of range # Catching any exception (not recommended for production code) try: # Risky operation result = eval(input("Enter an expression: ")) except Exception as e: print(f"An error occurred: {e}") # Perhaps log the error for debugging # When to use general exception handling def sample_function(): try: # Try specific exceptions first # Complex operations... pass except ValueError: # Handle ValueError specifically pass except TypeError: # Handle TypeError specifically pass except Exception as e: # Handle any other exceptions # Log error details for debugging import logging logging.error(f"Unexpected error: {e}", exc_info=True) raise # Re-raise the exception after logging
The Else and Finally Clauses
Python's exception handling includes optional else
and finally
clauses that
provide additional control.
# Using else clause (runs if no exception is raised) try: number = int(input("Enter a number: ")) result = 100 / number except ValueError: print("That's not a valid number!") except ZeroDivisionError: print("Cannot divide by zero!") else: # This runs only if no exceptions occurred print(f"The result is: {result}") # Using finally clause (always runs, regardless of exceptions) try: file = open("example.txt", "r") content = file.read() except FileNotFoundError: print("File not found!") finally: # This code always executes, even if an exception occurred # Good for cleanup operations if 'file' in locals() and not file.closed: file.close() print("File closed.") # Complete try-except-else-finally example try: # Attempt to perform an operation file = open("data.txt", "r") data = file.read() parsed_data = int(data) except FileNotFoundError: print("The file was not found.") # Create a file with default data with open("data.txt", "w") as f: f.write("0") parsed_data = 0 except ValueError: print("The file doesn't contain a valid number.") parsed_data = 0 else: # This runs if no exceptions were raised print(f"Successfully read data: {parsed_data}") finally: # This always runs for cleanup if 'file' in locals() and not file.closed: file.close() print("File closed.")
Advanced Exception Handling
Raising Exceptions
You can raise exceptions in your code using the raise
statement.
# Raising exceptions def divide(a, b): if b == 0: raise ZeroDivisionError("Cannot divide by zero") return a / b # Raising with a message def withdraw(account, amount): if amount > account.balance: raise ValueError(f"Insufficient funds: balance is {account.balance}, trying to withdraw {amount}") account.balance -= amount return account.balance # Re-raising exceptions try: # Some code that might raise an exception result = int("abc") except ValueError: print("Invalid integer, handling and re-raising...") raise # Re-raises the last exception # Transforming exceptions def get_user_data(user_id): try: # Try to retrieve user from database # This might raise various exceptions db = connect_to_database() user = db.query(f"SELECT * FROM users WHERE id = {user_id}") return user except Exception as e: # Transform into a more appropriate exception for the caller raise ValueError(f"Could not retrieve user {user_id}: {str(e)}") from e
Custom Exceptions
You can create your own exception classes for more specific error handling.
# Defining custom exceptions class CustomError(Exception): """Base class for custom exceptions in this module.""" pass class ValueTooSmallError(CustomError): """Raised when the input value is too small.""" def __init__(self, value, min_value, message="Value is too small"): self.value = value self.min_value = min_value self.message = f"{message}: {value} (minimum: {min_value})" super().__init__(self.message) class ValueTooLargeError(CustomError): """Raised when the input value is too large.""" def __init__(self, value, max_value, message="Value is too large"): self.value = value self.max_value = max_value self.message = f"{message}: {value} (maximum: {max_value})" super().__init__(self.message) # Using custom exceptions def validate_age(age): if age < 0: raise ValueTooSmallError(age, 0, "Age cannot be negative") if age > 150: raise ValueTooLargeError(age, 150, "Age is unrealistically high") return age # Handling custom exceptions try: user_age = int(input("Enter your age: ")) validated_age = validate_age(user_age) print(f"Your age is {validated_age}") except ValueTooSmallError as e: print(f"Error: {e}") except ValueTooLargeError as e: print(f"Error: {e}") except ValueError: print("Please enter a valid number.")
Context Managers and Exception Handling
Context managers (using the with
statement) provide a way to handle exceptions and resource management.
# Context managers for exception handling # Built-in examples with open("file.txt", "r") as file: # File is automatically closed even if an exception occurs content = file.read() # Custom context manager using a class class DatabaseConnection: def __init__(self, connection_string): self.connection_string = connection_string self.connection = None def __enter__(self): # Set up resource (open connection) print(f"Connecting to database: {self.connection_string}") self.connection = {"connected": True, "cursor": "db_cursor"} return self.connection def __exit__(self, exc_type, exc_val, exc_tb): # Clean up resource (close connection) if self.connection: print("Closing database connection") self.connection["connected"] = False self.connection = None # Handle specific exceptions if desired if exc_type is not None: print(f"An exception occurred: {exc_val}") # Return True to suppress the exception # Return False (or None) to let the exception propagate return False # Using the custom context manager try: with DatabaseConnection("mysql://localhost/mydb") as conn: # Use the database connection print(f"Connected: {conn['connected']}") # If an exception occurs here, __exit__ will be called # raise ValueError("Database query failed") except Exception as e: print(f"Error caught outside context manager: {e}") # Context manager using contextlib from contextlib import contextmanager @contextmanager def file_manager(filename, mode): try: # Setup - open the file file = open(filename, mode) # Yield the resource yield file finally: # Cleanup - close the file file.close() # Using the contextlib-based manager try: with file_manager("example.txt", "r") as file: content = file.read() except FileNotFoundError: print("File not found!")
Exception Handling Best Practices
Following are some best practices for effective exception handling in Python.
# 1. Be specific with exception types # Bad (too broad) try: # Some code pass except Exception: # Catches everything - might hide bugs pass # Good (specific) try: # Some code pass except ValueError: # Handle specifically pass except ZeroDivisionError: # Handle specifically pass # 2. Keep try blocks focused # Bad (too broad) try: file = open("data.txt") data = file.read() processed_data = process_data(data) result = calculate_result(processed_data) save_result(result) except Exception as e: print(f"An error occurred: {e}") # Good (focused) try: file = open("data.txt") except FileNotFoundError: print("File not found") return try: data = file.read() except IOError: print("Error reading file") file.close() return # Process data, handle other specific exceptions... # Remember to close the file # 3. Use context managers for resource cleanup # Bad file = open("data.txt") try: data = file.read() except Exception: # Handle exception pass # What if we forget to close the file? file.close() # Good with open("data.txt") as file: data = file.read() # File is automatically closed # 4. Don't suppress exceptions without good reason # Bad try: result = risky_operation() except Exception: # Silent failure is dangerous pass # Good try: result = risky_operation() except Exception as e: # Log the error logger.error(f"Error in risky_operation: {e}") # Possibly re-raise or handle gracefully raise # 5. Use exceptions for exceptional conditions, not flow control # Bad (using exceptions for control flow) def find_value(dictionary, key): try: return dictionary[key] except KeyError: return None # Good (check conditions without exceptions) def find_value(dictionary, key): if key in dictionary: return dictionary[key] return None # 6. Include helpful information in custom exceptions # Bad if value < 0: raise ValueError("Invalid value") # Good if value < 0: raise ValueError(f"Value must be positive, got: {value}") # 7. Use logging for debugging exceptions import logging try: # Some complex operation result = complex_operation() except Exception as e: # Log with traceback for debugging logging.error("Error in complex_operation", exc_info=True) # Display user-friendly message print("An error occurred. Please check the logs.")