8.5 Raising and Handling Exceptions in Python

In Python, you can raise exceptions when your program encounters unexpected situations or when you want to enforce certain conditions. Additionally, you can handle exceptions gracefully using try-except blocks, ensuring your program doesn’t crash and providing meaningful feedback when errors occur. This ability to control both raising and handling exceptions allows you to build robust and fault-tolerant applications.

In this section, we’ll cover how to raise exceptions manually, how to handle them effectively, and when to use custom exceptions.


8.5.1 Raising Exceptions

You can raise exceptions using the raise keyword when you want to trigger an error deliberately or enforce certain conditions in your code. This is useful for validating inputs or signaling when something goes wrong.

Syntax:

raise ExceptionType("Optional error message")
  • ExceptionType: The type of exception you want to raise (e.g., ValueError, TypeError, KeyError).
  • Optional error message: A custom message that provides additional information about the exception.

Example: Raising a Built-In Exception:

def divide(a, b):
    if b == 0:
        raise ValueError("Division by zero is not allowed")
    return a / b

# Testing the function
print(divide(10, 2))  # Output: 5.0
print(divide(10, 0))  # Raises ValueError: Division by zero is not allowed

In this example:

  • The divide function raises a ValueError when the second argument is zero to prevent division by zero.
  • The custom error message "Division by zero is not allowed" provides more context for the error.

8.5.2 Raising Custom Exceptions

You can define and raise custom exceptions by creating a class that inherits from Python’s built-in Exception class. This is helpful when you need specific error types for your application, making your code more descriptive and easier to debug.

Example: Creating and Raising a Custom Exception:

# Defining a custom exception
class InvalidAgeError(Exception):
    def __init__(self, age, message="Invalid age provided"):
        self.age = age
        self.message = message
        super().__init__(self.message)

# Using the custom exception
def check_age(age):
    if age < 0:
        raise InvalidAgeError(age, "Age cannot be negative")
    print(f"Age is valid: {age}")

# Testing the function
try:
    check_age(-5)
except InvalidAgeError as e:
    print(f"Caught an InvalidAgeError: {e}")

In this example:

  • InvalidAgeError is a custom exception that inherits from Exception.
  • The check_age function raises this custom exception if the input age is negative, providing a clear error message.
  • The exception is caught and handled in the except block.

8.5.3 Handling Exceptions with try-except

You can handle exceptions using the try-except block, which allows your program to continue running even if an error occurs. The code inside the try block is executed, and if an exception is raised, the control is passed to the corresponding except block.

Basic Syntax:

try:
    # Code that might raise an exception
    ...
except ExceptionType as e:
    # Code to handle the exception
    ...
  • try: Contains the code that may raise an exception.
  • except: Catches and handles the exception. You can specify the type of exception to catch and optionally assign it to a variable (e.g., as e).

Example: Handling an Exception:

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError as e:
        print(f"Error: {e}")
    else:
        print(f"Result: {result}")

# Test cases
safe_divide(10, 2)  # Output: Result: 5.0
safe_divide(10, 0)  # Output: Error: division by zero

In this example:

  • The division is attempted in the try block.
  • If a ZeroDivisionError occurs, it is caught in the except block, and an error message is printed.

8.5.4 Using else and finally in Exception Handling

In addition to try and except, you can use the else and finally blocks to handle situations where no exception occurs or where cleanup is needed regardless of whether an exception was raised.

  • else: This block is executed if no exceptions were raised in the try block.
  • finally: This block is always executed, whether an exception occurred or not. It is commonly used for cleanup actions, like closing files or releasing resources.

Example: Using else and finally:

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError as e:
        print(f"Error: {e}")
    else:
        print(f"Result: {result}")
    finally:
        print("Finished execution")

# Test cases
safe_divide(10, 2)  # Output: Result: 5.0, Finished execution
safe_divide(10, 0)  # Output: Error: division by zero, Finished execution

In this example:

  • The else block runs if no exceptions are raised, printing the result of the division.
  • The finally block runs after the try-except block, regardless of whether an exception occurred. It is used here to indicate the end of the function’s execution.

8.5.5 Raising Exceptions with raise Inside except

In some cases, you may want to catch an exception, perform some action, and then re-raise the exception to propagate it up the call stack. You can do this using the raise keyword inside an except block.

Example: Re-raising an Exception:

def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError as e:
        print("Handling ZeroDivisionError")
        raise  # Re-raise the exception

try:
    safe_divide(10, 0)
except ZeroDivisionError as e:
    print(f"Re-raised error: {e}")

In this example:

  • The ZeroDivisionError is caught and handled in the safe_divide function, where it is logged.
  • The exception is re-raised using raise to propagate the error further up the call stack, where it can be handled again.

8.5.6 Catching Multiple Exceptions

You can handle multiple exceptions in a single try block by specifying multiple exception types in an except clause. You can also chain multiple except blocks to handle each exception type separately.

Example: Catching Multiple Exceptions in One Block:

try:
    value = int("abc")  # Raises ValueError
except (ValueError, TypeError) as e:
    print(f"Caught an error: {e}")

In this example:

  • Both ValueError and TypeError are caught by the same except block, allowing you to handle different types of exceptions together.

Example: Handling Different Exceptions Separately:

try:
    my_list = [1, 2, 3]
    print(my_list[10])  # Raises IndexError
except IndexError:
    print("Index out of range")
except ValueError:
    print("Invalid value")

In this example:

  • IndexError and ValueError are handled in separate blocks, allowing different responses for each type of error.

8.5.7 Best Practices for Raising and Handling Exceptions

  1. Raise Exceptions for Exceptional Cases: Use raise to signal exceptional situations that cannot be handled within the current function or method (e.g., invalid input or unexpected system states).
  2. Handle Specific Exceptions: Catch specific exceptions rather than using a broad Exception clause. This allows for more fine-grained control over how different errors are handled.
  3. Use finally for Cleanup: Always use the finally block to release resources, close files, or clean up, ensuring that cleanup happens whether or not an exception occurs.
  4. Provide Helpful Error Messages: When raising exceptions, provide informative error messages to help users or developers understand what went wrong and how to fix it.
  5. Use Custom Exceptions for Specific Error Types: Create custom exception classes for specific error conditions in your application to make error handling clearer and more structured.

8.5.8 Summary

  • Raising exceptions: Use the raise keyword to trigger exceptions when your program encounters invalid situations or unexpected inputs. You can raise built-in or custom exceptions.
  • Handling exceptions: Use try-except blocks to catch and handle exceptions, preventing the program from crashing.
  • else and finally: Use the else block to run code when no exceptions occur

, and use finally to clean up resources regardless of whether an exception occurred.

  • Re-raising exceptions: Catch an exception and re-raise it if you want to propagate it further up the call stack.
  • Catching multiple exceptions: Handle different types of exceptions in a single except block or in separate blocks for more specific error handling.

By raising and handling exceptions properly, you can write more robust Python programs that deal with unexpected situations gracefully while providing helpful feedback and performing necessary cleanup tasks.