skip to content

Exception Hierarchy and Builtins

The tree every exception hangs from and what the common built-ins mean: BaseException versus Exception, ValueError, KeyError, OSError. Interviewers test whether you catch at the right level.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What is the difference between BaseException and Exception in Python?

level: juniorimportance: must knowfreq 68%

answer

  1. The tree has one root
  2. Not everything raisable is an error
  3. Three classes sit outside the error branch
  4. Ctrl-C and sys.exit are signals
  5. issubclass(SystemExit, Exception) is False

basics

~20 s

BaseException is the root of Python's exception tree; Exception is the branch that ordinary errors inherit from. SystemExit, KeyboardInterrupt and GeneratorExit sit outside Exception deliberately, so shutdown signals are not swept up with programming errors.

solid answer

~40 s

Every raisable object in Python inherits from `BaseException`. Directly under it sit four things: `Exception`, which nearly all real errors derive from, plus `SystemExit`, `KeyboardInterrupt` and `GeneratorExit`. Those three are not errors at all — they are control-flow signals. `SystemExit` is what `sys.exit()` raises to unwind and end the interpreter, `KeyboardInterrupt` is what a Ctrl-C signal turns into, and `GeneratorExit` is thrown into a suspended generator when it is closed. Keeping them off the `Exception` branch means a handler written for errors cannot accidentally swallow a shutdown request. Since 3.11 the same split repeats for groups: `BaseExceptionGroup` derives from `BaseException`, and `ExceptionGroup` derives from both it and `Exception`, so it can only carry `Exception` members. You can always check the relationship with `issubclass`.

code

python · 2 lines
python
for exc in (SystemExit, KeyboardInterrupt, GeneratorExit, ValueError):
    print(exc.__name__, issubclass(exc, Exception), issubclass(exc, BaseException))

go deeper

for a junior

Be ready to say that BaseException is the root and Exception is the branch under it, and to name at least one class that sits outside Exception. Knowing that Ctrl-C becomes KeyboardInterrupt is the concrete detail interviewers listen for.

for a middle

Explain the mechanics: matching is by class, so a subclass is caught by a handler naming its parent, and that is exactly why the three signals were kept off the Exception branch. Show that you can verify the tree with issubclass rather than reciting it.

for a senior

Demonstrate the operational consequence — a service whose handlers name BaseException stops responding to Ctrl-C and to shutdown requests. Be able to say when catching the root is legitimate: rollback or crash-reporting blocks that re-raise immediately.

for a principal

Own the codebase-wide convention: one rule about where errors may be rooted, enforced in review or by a linter, and a documented list of the handful of places broad catching is allowed. Explain how that rule keeps shutdown and cancellation reliable across every service in the fleet.

## One root, two meanings Python's exception tree has a single root, `BaseException`. Anything you `raise` must be an instance of it or a class derived from it; raising anything else is itself a `TypeError`. Immediately below the root the tree forks into two categories that mean very different things. The first branch is `Exception`. This is where essentially every *error* lives: `ValueError`, `TypeError`, `KeyError`, `OSError`, `AttributeError`, `RuntimeError`, and every class you or a library derives from `Exception`. These represent "something went wrong that a caller might reasonably want to handle". The second group is three siblings of `Exception`, sitting under `BaseException` directly: * **`SystemExit`** — raised by `sys.exit()`. It is not a crash; it is a request to unwind the stack, run `finally` blocks and cleanup, and end the interpreter with a status code carried in `args`. * **`KeyboardInterrupt`** — what Python's default handler for the interrupt signal (Ctrl-C) turns the signal into. * **`GeneratorExit`** — thrown into a suspended generator when it is closed, so its `finally` blocks can run. ## Why the split exists The split is a design decision about *catchability*. Exception matching in Python is by class: `except SomeClass` matches an instance of that class or of any subclass. That single rule is what makes the hierarchy load-bearing. Because `KeyboardInterrupt` is **not** a subclass of `Exception`, a handler written to deal with errors will never intercept a user's Ctrl-C, and a long-running loop stays interruptible even though it guards every iteration. The same reasoning protects `sys.exit()`: a library that mops up errors will not silently cancel your process shutdown. ```python try: raise KeyboardInterrupt("simulated Ctrl-C") except Exception: print("never reached") except BaseException as exc: print("caught", type(exc).__name__) ``` That is also why `BaseException` is the wrong thing to name in a routine handler: naming it opts you back into catching the three signals. The legitimate uses are narrow — a rollback or cleanup block that catches broadly, does its work and immediately re-raises, or a top-level crash reporter that re-raises after logging. ## What the root class gives you `BaseException` defines the machinery every exception shares: `args` (the constructor arguments, which is what the default `str()` renders), `__traceback__`, `with_traceback`, and since 3.11 `add_note` for attaching extra lines to a traceback. Whether an exception is an error or a signal, that plumbing is identical — the difference is purely one of position in the tree. ## Groups repeat the same shape Python 3.11 added exception groups, and they mirror the split exactly. `BaseExceptionGroup` derives from `BaseException` and can hold any exceptions; `ExceptionGroup` derives from *both* `BaseExceptionGroup` and `Exception`, and its constructor rejects members that are not `Exception` instances. Construct a `BaseExceptionGroup` whose members happen to all be `Exception` instances and Python hands you an `ExceptionGroup` instead. The invariant is preserved: a group that might contain a `KeyboardInterrupt` can never be caught by a handler asking for `Exception`. ## Checking it yourself The hierarchy is introspectable, and in an interview it is worth saying you would rather check than guess: ```python for exc in (SystemExit, KeyboardInterrupt, GeneratorExit, ValueError): print(exc.__name__, issubclass(exc, Exception)) ``` `SystemExit`, `KeyboardInterrupt` and `GeneratorExit` print `False`; `ValueError` prints `True`. `__mro__` on any exception class shows the full chain up to `BaseException` and `object`. ## The practical consequences Three things follow from the split, and they are what an interviewer is really probing. First, if you want a process to remain killable with Ctrl-C, do not name `BaseException` in handlers that continue execution. Second, `sys.exit()` inside a `try` is not magic — it raises, so `finally` blocks still run and a broad enough handler can cancel it; `os._exit` is the escape hatch that skips all of that. Third, when *you* define an exception class, derive it from `Exception`, not from `BaseException`: your errors are errors, not interpreter shutdown signals, and deriving from the root would make them invisible to every reasonable handler in the codebase. ## Raising, and what counts as raisable `raise` accepts either an exception class or an instance; given a class, Python instantiates it with no arguments, so `raise ValueError` and `raise ValueError()` are equivalent. Hand it anything that is not a `BaseException` subclass or instance — a string, a plain object — and Python raises `TypeError: exceptions must derive from BaseException` instead, which is the tree enforcing its own membership rule. When you want to settle where a class actually sits rather than argue about it, `SomeError.__mro__` prints the full chain from that class up through its bases to `BaseException` and `object`.

  • Where do ExceptionGroup and BaseExceptionGroup sit in that hierarchy?
    Both arrived in Python 3.11. `BaseExceptionGroup` derives from `BaseException` and can wrap any exceptions, including a `KeyboardInterrupt`. `ExceptionGroup` derives from both `BaseExceptionGroup` and `Exception`, and its constructor rejects non-`Exception` members. So the same guarantee holds for groups: a group that could carry a shutdown signal is never caught by a handler asking for `Exception`.
  • What happens when SystemExit propagates out of the main thread?
    The interpreter unwinds normally — `finally` blocks and context-manager exits run — then terminates with the status carried in the exception's `args`, printing no traceback. An integer becomes the exit status; a string is written to stderr with status 1; `None` means zero. Because it is an ordinary raise, a broad enough handler can catch it and cancel the shutdown, and `os._exit` bypasses the whole mechanism.
  • Should a custom exception ever derive from BaseException?
    Almost never. Deriving from `BaseException` makes your class invisible to every handler that asks for `Exception`, which is what nearly all calling code does. The only classes that legitimately sit there are the interpreter's own control-flow signals. Application and library errors derive from `Exception` — usually from one module-level base class of your own that itself derives from `Exception`.

Think of a building's alarm panel: fire alarms and burglar alarms are wired to the same box, but the 'evacuate the building' switch is wired separately on purpose, so that clearing a nuisance alarm can never cancel an evacuation.

saying these in an interview costs you the question

  • Claims every exception in Python inherits from Exception
  • Thinks KeyboardInterrupt is an Exception subclass
  • Believes sys.exit() ends the process without raising anything
  • Cannot name a single class outside the Exception branch
  • Treats BaseException and Exception as interchangeable names
  • Derives custom error classes from BaseException

context

open as a page

When does Python raise ValueError rather than TypeError?

level: juniorimportance: must knowfreq 62%

basics

~10 s

TypeError means the object is the wrong kind for the operation; ValueError means the type is right but the particular value is unusable. int([]) raises TypeError, int('twelve') raises ValueError.

open as a page

How do OSError subclasses such as FileNotFoundError relate to errno?

level: middleimportance: should knowfreq 48%

basics

~10 s

Python 3.3 gave the common operating-system error codes their own OSError subclasses, so you write except FileNotFoundError instead of inspecting err.errno. The errno attribute is still populated for codes without a dedicated class.

open as a page

Why can KeyboardInterrupt interrupt a shared-cache update midway and leave it inconsistent?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Ctrl-C sets a flag that the interpreter checks between bytecodes, so KeyboardInterrupt is raised at an arbitrary point inside a multi-step update. Because it derives from BaseException, error handlers written for Exception never run the rollback.

open as a page