Back to Blog
Python

Understanding Python Exception Hierarchy

python exception hierarchy: Learn how Python's exception hierarchy works, why it matters for error handling, and how to catch exceptions precisely without masking bugs.

PythonException HandlingError HandlingException HierarchyPython Development
Diagram showing Python exception hierarchy from BaseException to specific exceptions like ValueError and TypeError.

When an exception propagates in Python, the interpreter walks the exception hierarchy to find a matching except clause. The python exception hierarchy determines which handler runs, so a small mistake in the hierarchy can turn a precise error into a silent failure or an uncaught crash. This article explains how the hierarchy is structured, why it matters for error handling, and how to use it to write robust code.

The Root of the Hierarchy

Every exception in Python is an instance of a class that inherits from BaseException. This is the root of the entire hierarchy. When you raise an exception, you are creating an instance of a class that ultimately derives from BaseException. The interpreter uses this inheritance chain to match the raised exception against the except clauses in the current scope.

Consider the simplest case:

raise ValueError("invalid input")

The ValueError class inherits from Exception, which in turn inherits from BaseException. When the exception is raised, Python checks each except clause in order, from top to bottom, and selects the first one whose exception type is a superclass of the raised exception type.

BaseException vs Exception

The most important split in the hierarchy is between BaseException and Exception. BaseException is the base for all exceptions, but it also includes a few special cases that are not meant to be caught by normal application code. The most notable are SystemExit, KeyboardInterrupt, and GeneratorExit. These inherit directly from BaseException, not from Exception.

Here is the critical distinction:

try: raise KeyboardInterrupt except Exception: print("This will not catch KeyboardInterrupt") except BaseException: print("This will catch it")

Because KeyboardInterrupt does not inherit from Exception, a bare except Exception will not catch it. This is intentional: KeyboardInterrupt signals that the user pressed Ctrl+C, and you generally want the program to exit. Similarly, SystemExit is raised by sys.exit() and should not be swallowed by a generic error handler.

For most application code, you should catch Exception rather than BaseException. Catching BaseException will also catch SystemExit and KeyboardInterrupt, which can make your program impossible to interrupt or cause it to ignore explicit exit requests.

Built-in Exception Groups

Python's standard library defines a rich set of built-in exceptions that form a tree under Exception. The most common ones are grouped by the kind of error they represent. For example, ArithmeticError is the parent of ZeroDivisionError, OverflowError, and FloatingPointError. LookupError is the parent of IndexError and KeyError. OSError is the parent of FileNotFoundError, PermissionError, and many others.

The following table shows a few representative branches:

ExceptionParentTypical Cause
ValueErrorExceptionInvalid argument value
TypeErrorExceptionWrong type for an operation
IndexErrorLookupErrorSequence index out of range
KeyErrorLookupErrorMissing dictionary key
ZeroDivisionErrorArithmeticErrorDivision by zero
FileNotFoundErrorOSErrorFile does not exist

This hierarchy is not just a taxonomy. It directly affects how you write except clauses. If you catch LookupError, you will also catch IndexError and KeyError. If you catch OSError, you will catch FileNotFoundError, PermissionError, and other OS-related errors.

Catching by Specific Type

The most common mistake in exception handling is catching too broadly. A bare except: catches everything, including KeyboardInterrupt and SystemExit. A slightly better version, except Exception:, still catches every application-level error, which often hides bugs. The python exception hierarchy gives you the tools to be precise.

Instead of:

try: value = int(user_input) except Exception: print("Invalid input")

Use:

try: value = int(user_input) except ValueError: print("Invalid input")

The second version only catches ValueError, which is what int() raises when the input cannot be parsed. It does not catch TypeError if user_input is None or an object without __int__, nor does it catch a KeyboardInterrupt that arrives while waiting for input. This precision makes the error handling predictable and debuggable.

When you need to handle multiple related exceptions, you can group them in a tuple:

try: data = config["path"] with open(data) as f: content = f.read() except (KeyError, FileNotFoundError) as exc: print(f"Configuration error: {exc}")

Here, both KeyError and FileNotFoundError are caught, but nothing else. If the file exists but you lack permission, a PermissionError will propagate, which is often the correct behavior because it is a different problem.

Custom Exceptions and the Hierarchy

When you define your own exceptions, you should inherit from Exception (or from a more specific built-in exception if it fits). This keeps your exceptions inside the normal application-error branch and allows callers to catch them with except Exception if they need to.

class ConfigurationError(Exception): pass class MissingSettingError(ConfigurationError): pass class InvalidSettingError(ConfigurationError): pass

Now you can catch ConfigurationError to handle any configuration problem, or catch MissingSettingError specifically if you need to differentiate. This is the same principle as the built-in hierarchy: you can design your own tree to match the granularity of your error domain.

A common pattern is to create a base exception for a module or package, then derive more specific exceptions from it. This allows library users to catch the base exception without knowing every possible failure mode, while still giving them the option to catch specific subtypes.

Hierarchy and Compatibility

The exception hierarchy is part of Python's public API. It is stable across versions, but it does evolve. For example, FileNotFoundError was introduced in Python 3.3 as a subclass of OSError. Code that catches OSError will continue to work, but code that expects IOError (which is an alias for OSError in Python 3) needs to be updated if it relies on the old name.

When you write a library, you should document which exceptions your functions raise. If you raise a custom exception that inherits from Exception, callers can catch it reliably. If you raise a built-in exception like ValueError, callers can rely on the standard semantics. Changing the exception type in a future version is a breaking change, so it is worth choosing the base class carefully from the start.

Performance and Runtime Considerations

Exception handling in Python is not free. Raising and catching an exception involves constructing the exception object, walking the stack, and searching for a matching handler. The cost is usually small compared to I/O or network operations, but it can matter in tight loops or high-frequency code paths.

A common performance pitfall is using exceptions for control flow. For example, checking whether a dictionary key exists by catching KeyError is slower than using if key in dict: when the key is usually present. The exception mechanism is optimized for the exceptional case, not for the common case. If you expect a condition to be rare, using an exception is fine. If it is part of normal logic, prefer an explicit check.

Another consideration is the cost of broad exception handlers. A handler like except Exception: will match almost any error, which means the interpreter has to check the hierarchy for every raised exception. In practice, the difference is negligible because the matching is a simple isinstance check. The real cost is in debugging: a broad handler can mask the original error and make production issues harder to trace.

To keep your exception handling both correct and maintainable, follow the hierarchy: catch the most specific exception that you can handle, and let everything else propagate. This makes the behavior of your code predictable and keeps the python exception hierarchy working in your favor.

python exception hierarchy: Practical Usage and Code Example | RYUSLOG DEV