Skip to main content

Understanding the Python Exception Hierarchy

Β· 7 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

🌳 Understanding the Python Exception Hierarchy​

In Python, all exceptions are organized into a strict, single-rooted hierarchy of classes. Understanding this hierarchy is not just academic; it is fundamental to writing reliable exception handlers. When you catch an exception, you are actually catching that specific class and all classes that inherit from it.

This article breaks down the core structure of the Python exception hierarchy and demonstrates how inheritance dictates the behavior of your except blocks.

1. The Root of All Exceptions: BaseException​

Every single exception in Python inherits from the BaseException class. This includes exceptions meant to signal program termination, such as system exits or interpreter interrupts, which should generally not be caught.

The primary exceptions that your application should handle (all runtime errors) inherit from Exception, which is a subclass of BaseException.

ClassPurposeShould be Caught?
BaseExceptionThe root class. For exceptions intended to terminate the program.NO (Almost Never)
ExceptionThe base class for all non-fatal exceptions and standard errors.YES

Examples of Exceptions Inheriting from BaseException (but NOT Exception)​

  • KeyboardInterrupt (User presses Ctrl+C)
  • SystemExit (Program attempts to exit using sys.exit())
  • GeneratorExit

2. The Core Hierarchy: Standard Library Exceptions​

Most standard exceptions you encounter belong to one of a few major branches under the main Exception class.

Base ClassPurposeCommon Children (Examples)
StandardError (Inherited by all other runtime errors)Base class for exceptions that occur during standard application execution.N/A (Internal use)
ArithmeticErrorBase class for numerical errors.ZeroDivisionError, OverflowError
LookupErrorBase class for errors when looking up a value in a sequence or mapping.KeyError, IndexError
OSErrorBase class for system-related errors (file I/O, network).FileNotFoundError, PermissionError
ValueErrorBase class for errors when a function receives an argument of the correct type but an inappropriate value.UnicodeError, JSONDecodeError

3. Practical Impact: How Inheritance Affects except Blocks​

The most important takeaway from the hierarchy is that catching a parent exception class will also catch all its descendants. This allows for flexible and consolidated error handling.

A. Catching Multiple Specific Errors​

You can catch multiple distinct, specific exceptions using a tuple:

try:
data = {'a': 10}
data['b'] # KeyError
result = 10 / 0 # ZeroDivisionError

except (KeyError, ZeroDivisionError) as e:
# Catches either a KeyError or a ZeroDivisionError
print(f"Handled a specific lookup or division error: {type(e).__name__}")

B. Catching by Parent Class (Consolidated Handling)​

By catching a parent class, you can handle related errors uniformly. This is cleaner than catching every child individually.

def access_sequence(sequence, index):
try:
# Can raise IndexError (list) or KeyError (dict)
return sequence[index]

except LookupError as e:
# LookupError is the common parent of both IndexError and KeyError
# This single block handles both!
print(f"Error: Invalid access key/index ({type(e).__name__}).")
return None

access_sequence([1, 2, 3], 5) # Triggers IndexError -> Caught by LookupError
access_sequence({'a': 1}, 'b') # Triggers KeyError -> Caught by LookupError

C. Order Matters: The Rule of Specificity​

When you use multiple except blocks, they are evaluated from top to bottom. If a parent class is listed before its child class, the parent will catch the exception, and the specific handler for the child will never be reached.

The Rule: Always list the most specific (child) exception blocks first, followed by the most general (parent) blocks.

❌ Incorrect Order (KeyError handler is unreachable)βœ… Correct Order (Specific first)
python<br>try:<br> data['key']<br>except LookupError:<br> print("Parent caught it.")<br>except KeyError: # UNREACHABLE!<br> print("Child caught it.")<br>python<br>try:<br> data['key']<br>except KeyError:<br> print("Child caught it.")<br>except LookupError:<br> print("Parent is the fallback.")<br>

4. Custom Exceptions and the Hierarchy​

When defining your own custom exceptions, you should ensure they fit cleanly into the hierarchy by inheriting from a relevant standard class.

If your custom exception is tied to application logic (e.g., a login failure), it should always inherit from Exception or a direct subclass of it.

# 1. Inherit directly from Exception for a high-level application error
class SecurityError(Exception):
pass

# 2. Inherit from a specific standard error if the failure relates to system input/data
class InvalidUserDataError(ValueError):
pass

def process_login(user):
if user.is_banned:
raise SecurityError("User is permanently blocked.")
if user.password == "":
# This will be caught by any upstream code catching ValueError!
raise InvalidUserDataError("Password cannot be empty.")

By inheriting from ValueError, any code that currently handles generic data validation errors (ValueError) will automatically start handling InvalidUserDataError as well, making your custom exceptions instantly compatible with existing error handling logic.


Sources and Further Reading​

  1. Python Documentation - Built-in Exceptions (The definitive hierarchy list)
  2. Python Documentation - User-defined Exceptions
  3. Real Python - Python Exception Handling (Focus on specificity)
  4. Real Python - Python Custom Exceptions