8.4 Exception Hierarchy in Python

In Python, exceptions are objects that represent errors during the execution of a program. These exceptions are organized in a structured exception hierarchy, where all exception classes inherit from a common base class. Understanding the exception hierarchy is important for catching and handling specific exceptions in a more controlled way, allowing you to address different types of errors appropriately.

In this section, we will explore Python’s exception hierarchy, focusing on the relationships between the various exception classes, and how to work with them effectively.


8.4.1 The Base Class: BaseException

At the top of Python’s exception hierarchy is the BaseException class. All built-in exceptions inherit from BaseException, making it the root class for all exceptions in Python.

  • BaseException: The base class for all exceptions. You typically do not catch this exception directly unless you are writing specialized error-handling code.

Example:

try:
    raise Exception("This is a custom exception")
except BaseException as e:
    print(f"Caught BaseException: {e}")

In this example:

  • BaseException is used to catch all types of exceptions, but it's usually not recommended because it can catch exceptions that should generally be handled differently (like KeyboardInterrupt or SystemExit).

8.4.2 Common Subclasses of BaseException

Directly inheriting from BaseException are a few important exceptions, most notably Exception, SystemExit, KeyboardInterrupt, and GeneratorExit. These exceptions serve specific purposes:

  • Exception: The most commonly used base class for all built-in, user-defined, and custom exceptions. You will usually catch exceptions derived from Exception.
  • SystemExit: Raised when the program is exiting (e.g., when calling sys.exit()).
  • KeyboardInterrupt: Raised when the user interrupts program execution, typically by pressing Ctrl+C.
  • GeneratorExit: Raised when a generator or coroutine is closed.

8.4.3 The Exception Class

The Exception class is the parent class for most exceptions that are raised during the execution of a program. You will catch exceptions derived from this class for most error handling in Python.

  • Exception: The base class for all standard exceptions. You can catch this class to handle most common errors, but it’s usually better to catch more specific exceptions.

Example:

try:
    x = 1 / 0
except Exception as e:
    print(f"Caught Exception: {e}")

In this example:

  • Exception catches the specific exception ZeroDivisionError because it inherits from Exception.

8.4.4 Common Built-In Exception Classes

Python provides many built-in exception classes that inherit from Exception. Here are some of the most commonly used ones:

  • ArithmeticError: Base class for errors related to numeric operations.
    • ZeroDivisionError: Raised when division by zero is attempted.
    • OverflowError: Raised when a numerical result is too large to be represented.
    • FloatingPointError: Raised for floating-point arithmetic errors.
  • LookupError: Base class for errors related to lookups (e.g., accessing elements in sequences or mappings).
    • IndexError: Raised when an index is out of range in a list, tuple, or similar.
    • KeyError: Raised when a dictionary key is not found.
  • ValueError: Raised when an operation receives an argument of the right type but with an inappropriate value.
  • TypeError: Raised when an operation or function is applied to an object of inappropriate type.
  • NameError: Raised when a local or global name is not found.
    • UnboundLocalError: Raised when a local variable is referenced before it has been assigned a value.
  • IOError: Raised when an input/output operation fails (e.g., file handling). In Python 3, IOError is an alias for OSError.
  • OSError: Base class for operating system-related errors (e.g., file system errors, permission errors).
    • FileNotFoundError: Raised when a file or directory is requested but cannot be found.
  • RuntimeError: Raised when an error does not fall into any other category.
    • RecursionError: Raised when the maximum recursion depth is exceeded.

Example of Specific Exceptions:

try:
    my_list = [1, 2, 3]
    print(my_list[10])  # Raises IndexError
except IndexError as e:
    print(f"IndexError caught: {e}")

In this example:

  • IndexError is caught specifically, allowing more precise handling of the error.

8.4.5 Catching Multiple Exceptions

You can handle multiple exceptions in a single try block by specifying multiple exception types in an except clause.

Example: Catching Multiple Exceptions:

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

In this example:

  • Both ValueError and TypeError are caught by the same except block.

You can also chain multiple except blocks to handle each exception type separately.

Example:

try:
    value = int("abc")
except ValueError:
    print("Caught a ValueError")
except TypeError:
    print("Caught a TypeError")

In this case:

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

8.4.6 Custom Exceptions

In Python, you can define your own exceptions by creating a custom class that inherits from the Exception class or one of its subclasses. This is useful when you need to raise specific errors in your application.

Example: 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 validate_age(age):
    if age < 0:
        raise InvalidAgeError(age)
    print(f"Valid age: {age}")

try:
    validate_age(-5)
except InvalidAgeError as e:
    print(f"Caught custom exception: {e}")

In this example:

  • The custom exception InvalidAgeError inherits from Exception and is raised when the input age is invalid (e.g., negative).
  • The InvalidAgeError includes custom attributes and a default message.

8.4.7 The Complete Python Exception Hierarchy

Here’s a simplified version of Python’s exception hierarchy:

BaseException
 ├── SystemExit
 ├── KeyboardInterrupt
 ├── GeneratorExit
 └── Exception
      ├── ArithmeticError
      │    ├── ZeroDivisionError
      │    ├── OverflowError
      │    └── FloatingPointError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── ValueError
      ├── TypeError
      ├── NameError
      │    └── UnboundLocalError
      ├── OSError
      │    └── FileNotFoundError
      ├── RuntimeError
      │    └── RecursionError
      └── ...

8.4.8 Summary

  • BaseException is the top-level class in Python’s exception hierarchy, with Exception as its most common subclass for handling errors in programs.
  • Common built-in exceptions like ValueError, TypeError, IndexError, and KeyError inherit from Exception and are raised in different situations.
  • You can catch multiple exceptions either by combining them in a single except block or by using multiple except blocks.
  • You can create custom exceptions by inheriting from the Exception class when you need more specific error handling in your programs.
  • Python’s exception hierarchy helps organize and categorize errors, allowing you to handle specific types of exceptions in a more controlled manner.

By understanding Python’s exception hierarchy, you can write more robust code that handles different types of errors effectively, and you can extend this system by defining your own exceptions for specialized use cases.