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 whensys.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 likeConnectionResetErrorandConnectionRefusedError) — 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 ValueErrorValueError, 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 exceptionBecause 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.