Understanding Python's Exception Hierarchy

Every exception you've caught throughout this series — ValueError, KeyError, ZeroDivisionError — belongs to a structured python exception hierarchy, not a flat, unrelated list. Understanding the BaseException vs Exception distinction, and the branches in between, directly shapes how broadly or narrowly your except blocks actually catch errors.

Introduction: Exceptions Form a Family Tree

Every exception in Python is an instance of a class, and that class inherits — directly or indirectly — from BaseException. This means Python's exceptions form a structured inheritance tree, not a flat, disconnected list of error types.

Why this matters practically

Because exceptions inherit from one another, catching a general exception type automatically catches all of its more specific subclasses too. except OSError: catches FileNotFoundError, PermissionError, ConnectionError, and every other OSError subclass — you don't need to list each one individually, because they're all, structurally, a kind of OSError.

A preview

This article walks the tree from the root (BaseException) down through the major branches, to the specific, familiar exception types you've already been catching throughout this series — and covers exactly how that structure shapes the ordering rules for except blocks.

The Root: BaseException vs. Exception

BaseException: the true root

BaseException sits at the very top of Python's exception hierarchy — every other exception, without exception (no pun intended), inherits from it directly or indirectly.

The special cases directly beneath it

A small handful of exceptions sit directly under BaseException, deliberately kept outside of Exception:

  • SystemExit — raised when sys.exit() is called, signaling the program should terminate.

  • KeyboardInterrupt — raised when a user presses Ctrl+C.

  • GeneratorExit — raised inside a generator when it's closed.

This separation is deliberate, and genuinely important: keeping these three outside of Exception means that catching Exception broadly — a common, generally reasonable pattern — won't accidentally swallow a user's Ctrl+C, or interfere with a normal, intentional program exit. This is exactly the reasoning behind the "avoid bare except:" guidance covered in the earlier try/except/finally article: a bare except: catches BaseException and everything beneath it, including these three signals that almost never should be silently caught by ordinary application error-handling code.

Exception: the practical base for everything else

Exception is the base class for almost all non-fatal, recoverable errors — the ones you actually want to catch and handle in everyday code. It's also the correct parent class for your own custom exceptions, exactly as covered in the earlier custom exceptions article. For nearly all practical purposes, Exception — not BaseException — is the root you should be thinking about day to day.

Touring the Major Branches

Several intermediate base classes group related, specific exceptions together, and it's worth knowing the major ones.

ArithmeticError

Parent of exceptions related to numeric computation problems:

  • ZeroDivisionError — division or modulo by zero.

  • OverflowError — a numeric result too large to represent.

  • FloatingPointError — a floating-point operation failure (rare in practice, since Python's default float handling doesn't typically raise this).

LookupError

Parent of exceptions raised when a lookup (by index or key) fails:

  • IndexError — an invalid position in a sequence, covered in the earlier lists and string slicing articles.

  • KeyError — a missing key in a dictionary, covered in the earlier dictionaries article.

OSError

Parent of exceptions related to system-level, operating-environment problems — file access, network connections, and similar:

  • FileNotFoundError — a file that doesn't exist.

  • PermissionError — insufficient permissions to perform an operation.

  • ConnectionError (itself a parent of further subtypes like ConnectionResetError and ConnectionRefusedError) — network connection problems.

A visual excerpt of the tree
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── OverflowError
    │   └── FloatingPointError
    ├── LookupError
    │   ├── IndexError
    │   └── KeyError
    ├── OSError
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── ConnectionError
    │       ├── ConnectionResetError
    │       └── ConnectionRefusedError
    ├── ValueError
    ├── TypeError
    └── ... (many more)

This is a deliberately trimmed excerpt — the real tree is considerably larger — but it shows the essential structure: FileNotFoundError is genuinely a kind of OSError, and ZeroDivisionError is genuinely a kind of ArithmeticError, not just a coincidentally similar, unrelated type.

Why these intermediate classes exist

These grouping classes let code catch an entire category of related errors without listing every specific subtype individually:

try:
    with open("data.txt") as f:
        c f.read()
except OSError as e:
    print(f"A file or system-level problem occurred: {e}")

This single except OSError: correctly catches FileNotFoundError, PermissionError, and any other OSError subclass — genuinely useful when your actual handling logic ("something went wrong accessing this file or resource") is the same regardless of exactly which specific OSError subtype occurred.

Why Hierarchy Order Matters in except Blocks

The practical rule

This connects directly to the hierarchy structure covered above: when using multiple except clauses, specific exceptions must be listed before general ones — Python checks except blocks top to bottom and stops at the very first class that matches, exactly as covered in the earlier multiple exceptions article. Because subclasses match their parent class's except too, a general class listed first will silently intercept everything, including the more specific exceptions you meant to handle separately.

A concrete example: correct order
try:
    value = int("abc")
except ValueError:
    print("Specifically caught a ValueError")
except Exception:
    print("Caught some other, more general exception")
Specifically caught a ValueError

ValueError, listed first and being more specific, correctly catches the error before Python ever reaches the broader except Exception: clause below it.

What silently breaks if the order is reversed
try:
    value = int("abc")
except Exception:
    print("Caught some other, more general exception")
except ValueError:
    print("Specifically caught a ValueError")
Caught some other, more general exception

Because ValueError is a subclass of Exception, the broader except Exception: clause — now listed first — matches immediately, and Python never even reaches the more specific except ValueError: clause below it. This isn't an error or a warning — it's a silent, easy-to-miss logic bug, where your intended specific handling simply never runs, because a broader clause earlier in the chain quietly absorbed it first. This is exactly the same "order matters" principle covered for elif chains in the earlier conditionals article, applied here specifically to exception class hierarchies instead of plain value comparisons.

Reinforcing: avoid catching BaseException directly

Worth restating clearly here, now that the full hierarchy context is in view: catching BaseException directly in regular application code is discouraged for exactly the same reason a bare except: is discouraged — it's simply too broad, silently catching SystemExit and KeyboardInterrupt right alongside everything else, interfering with a user's ability to cleanly stop your program or exit it intentionally.

Exploring the Hierarchy Yourself, and Modern Additions

Using inspect to see the live tree

Rather than memorizing the hierarchy, you can inspect it programmatically, directly from a running Python session, using __subclasses__() recursively:

def print_exception_tree(cls, indent=0):
    print("  " * indent + cls.__name__)
    for subclass in cls.__subclasses__():
        print_exception_tree(subclass, indent + 1)

print_exception_tree(BaseException)

This walks the actual, currently-loaded exception hierarchy in your running Python environment — genuinely useful for confirming exactly where a specific exception type sits, or for discovering exception types you might not already be aware of, rather than relying purely on documentation or memory. Python's inspect module also provides inspect.getclasstree(), which can build a similarly structured, nested representation of a class hierarchy programmatically, if you need it in a different format for further processing.

Newer additions: ExceptionGroup and BaseExceptionGroup

As covered in the earlier multiple exceptions article, Python 3.11 introduced ExceptionGroup (and its counterpart, BaseExceptionGroup) for representing several unrelated exceptions raised together as a single unit. These are genuinely part of the exception hierarchy itself — ExceptionGroup inherits from Exception, and BaseExceptionGroup inherits from BaseException — which means they can be caught with a regular except clause like any other exception, in addition to being specifically recognized by the newer except* syntax, which matches sub-groups within an exception group based on the types of exceptions they actually contain.

try:
    raise ExceptionGroup("multiple problems", [ValueError("bad value"), TypeError("bad type")])
except Exception as e:   # a plain except Exception still catches the whole group
    print(type(e))   # <class 'ExceptionGroup'>

Their placement in the hierarchy — ExceptionGroup under Exception, BaseExceptionGroup under BaseException — is a deliberate design choice, keeping them fully consistent with the rest of Python's exception-handling model, rather than functioning as some entirely separate, special-cased mechanism.

The practical takeaway

Understanding this hierarchy isn't an academic exercise — it directly shapes how broadly or narrowly a given except block actually catches errors. Knowing that FileNotFoundError is a kind of OSError, that IndexError and KeyError both descend from LookupError, and that Exception deliberately excludes KeyboardInterrupt and SystemExit, is what lets you write except clauses that catch precisely what you intend — no broader, and no narrower — rather than guessing at exception relationships or over-catching out of uncertainty about how specific and general error types actually relate to one another.