Raising Exceptions

basic
Published

January 24, 2024

The Basics of Raising Exceptions

Raising an exception in Python uses the raise keyword followed by the exception object you want to trigger. Python offers a rich hierarchy of built-in exceptions, but you can also create custom exceptions to match your application’s specific needs.

Let’s start with a simple example using a built-in exception, ValueError:

def validate_age(age):
  if age < 0:
    raise ValueError("Age cannot be negative")
  print(f"Age is valid: {age}")

try:
  validate_age(-5)
except ValueError as e:
  print(f"Error: {e}")

validate_age(30)

This code snippet defines a function validate_age that checks if the input age is valid. If the age is negative, it raises a ValueError with a descriptive message. The try...except block catches the exception and prints an informative error message.

Raising Custom Exceptions

For more specific error handling, creating custom exceptions is beneficial. This improves code readability and allows for more targeted exception handling. Custom exceptions are typically defined as classes that inherit from built-in exception classes like Exception or more specific ones like ValueError or TypeError.

class InsufficientFundsError(Exception):
  pass

class Account:
  def __init__(self, balance):
    self.balance = balance

  def withdraw(self, amount):
    if self.balance < amount:
      raise InsufficientFundsError("Insufficient funds in the account.")
    self.balance -= amount
    print(f"Withdrawal successful. New balance: {self.balance}")

account = Account(100)
try:
  account.withdraw(150)
except InsufficientFundsError as e:
  print(f"Error: {e}")

account.withdraw(50)

This example demonstrates a custom exception InsufficientFundsError. The Account class uses this exception to signal when a withdrawal exceeds the available balance.

Raising Exceptions with Arguments

You can provide additional context to exceptions by passing arguments to the exception constructor. This allows you to include specific details about the error, such as file names, line numbers, or other relevant data.

def process_file(filename):
  try:
    with open(filename, 'r') as f:
      # ... file processing logic ...
      pass
  except FileNotFoundError as e:
    raise FileNotFoundError(f"File not found: {filename}") from e


try:
    process_file("nonexistent_file.txt")
except FileNotFoundError as e:
    print(f"An error occurred: {e}")

In this improved process_file function, if a FileNotFoundError occurs, a more informative exception is raised, including the filename. The from e clause helps preserve the original traceback, aiding debugging.

Re-raising Exceptions

Sometimes, you might want to handle an exception partially and then re-raise it to be handled by a higher level of the call stack. This is achieved using the raise keyword without specifying an exception:

try:
    # Some code that might raise an exception
    raise ValueError("Something went wrong")
except ValueError as e:
    print("Caught a ValueError!")
    # Perform some cleanup or logging here
    raise  # Re-raises the ValueError

This allows you to perform actions such as logging the error before passing it further up.

Choosing the Right Exception

Selecting the appropriate exception type is critical. Using built-in exceptions where suitable avoids unnecessary custom exception classes and improves code clarity. Ensure that the exception message clearly communicates the error’s nature. Avoid overly generic exceptions like Exception unless absolutely necessary, as more specific exceptions improve debugging and error handling.