Exception Handling

Exceptions are unexpected events that have occurred in the execution of a program, like an alarm that goes off when unexpected events happen while running code.

These events could be caused by several factors:

  • Logical Errors: i.e., typo, missing data, wrong data, etc.
  • Unforeseen Events: trying to open a file that doesn’t exist.
 1try:
 2  # Code that might cause an error (like accessing a missing file)
 3except FileNotFoundError:
 4  # Do this if a FileNotFoundError happens
 5except TypeError:
 6  # Do this if a TypeError happens
 7else:
 8  # Do this if NO exception happened in the 'try' block
 9finally:
10  # This ALWAYS runs, error or no error

Common Exception Types

Class Description
Exception The base class for most error types.
AttributeError Raised when attempting to access a non-existent attribute of an object (e.g., calling a method the object does not have).
EOFError Occurs when input() is called without an available line to read, often indicating an unexpected end of file.
IOError Signals an error related to input/output operations, such as file handling. Common causes include attempting to open a non-existent file or insufficient permissions.
IndexError Raised when accessing a sequence, a list, with an index that is out of range.
KeyError Occurs when attempting to access a dictionary key that does not exist.
KeyboardInterrupt Raised when the user interrupts program execution, typically by pressing Ctrl+C
NameError Indicates that a local or global name (variable, function, etc.) is not found. Often due to typos or referencing names before definition.
StopIteration Raised by an iterators __next__() method to signal the end of the iteration.
TypeError Occurs when an operation or function is applied to an object of an inappropriate type, such as attempting to add a string to an integer.
ValueError Raised when an operation or function receives an argument of the correct type but with an inappropriate value. For instance, attempting to compute the square root of a negative number.
ZeroDivisionError Signals an attempt to divide by zero, an undefined mathematical operation.

Raising an Exception

  1. raise: This keyword throws the exception, halting normal execution.
  2. ValueError: This is a built-in exception type that’s perfect for signaling inappropriate values.
  3. "x cannot be negative": This message explains the problem, helping you debug later.
1def calculate_square_root(x):
2  if x < 0:
3    raise ValueError(f"{x} cannot be negative")
4  # ... rest of the logic goes here ... 

Why is this useful?

  • Early Detection: Find problems quickly.
  • Clean Code: Separate error handling from regular logic.
  • Maintainability: Makes your code easier to understand and fix in the future.

Custom Exceptions:

While built-in exceptions like ValueError are useful for general cases, custom exceptions give you the power to pinpoint very specific issues within your code, this is useful for debugging or testing.

This makes debugging significantly easier. Furthermore, as your projects expand, custom exceptions become invaluable for organizing and categorizing errors, leading to improved code structure and maintainability. This targeted approach extends to error handling itself: you can write except blocks designed to catch only your custom exceptions, enabling more precise and effective error responses.

  1. Inheritance: Always inherit from Exception (directly or indirectly) to leverage Python’s exception system.
1class MyCustomError(Exception):
2   # Can be empty for simple cases
3   pass  
  1. Constructor (__init__): Optional, but useful for storing extra error information.
1class InvalidAgeError(Exception):
2   def __init__(self, age, message="Age must be between 0 and 120"):
3       super().__init__(message) 
4       self.age = age

Example

 1class InsufficientFundsError(Exception):
 2    def __init__(self, balance, amount):
 3        super().__init__(f"Insufficient funds: Balance={balance}, Attempted withdrawal={amount}")
 4        self.balance = balance
 5        self.amount = amount
 6
 7def withdraw(balance, amount):
 8    if amount > balance:
 9        raise InsufficientFundsError(balance, amount)
10    return balance - amount
11
12try:
13    new_balance = withdraw(100, 150) 
14except InsufficientFundsError as e:
15    # Insufficient funds: Balance=100, Attempted withdrawal=150
16    print(e)  
17    print("Please try a smaller amount.") 
18else:
19    print(f"Withdrawal successful. New balance: {new_balance}") 

Best Practices:

  • Avoiding overly broad exception catches (e.g., catching Exception).
  • Providing informative error messages to aid debugging.
  • Using exceptions appropriately.

Notes:

  • try: Wrap risky code. Could crash your program.
  • except: Handle specific errors if they occur.
  • else: Run code if no errors.
  • finally: Clean up, always runs.
  • raise: throws an exception.

Read: Python Inheritance