skip to content

The Exception Model

Raising and catching a single exception: the BaseException hierarchy, the full try/except/else/finally flow, chaining, custom classes and the EAFP idiom. The core of any error-handling round.

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

questions

page 1 of 2

How do you define a custom exception class in Python, and what should it inherit from?

level: juniorimportance: must knowfreq 70%

answer

  1. Exceptions are just classes
  2. Inheritance decides what callers can catch
  3. One base class per module
  4. Derive from Exception, not the root
  5. Docstring body is enough

basics

~20 s

Write a class that inherits from Exception; an empty body with a docstring is enough. Name it with an Error suffix, give the module one base class of its own, and derive every specific error from that base.

solid answer

~40 s

A custom exception is an ordinary class whose only hard requirement is that it derives from `Exception` (or, more broadly, from `BaseException`) — `class PickListError(Exception):` plus a docstring is a complete definition, no registration or metaclass needed. Convention is an `Error`-suffixed name and a docstring saying when it is raised. Give a module or package **one** base class of its own and derive the specific errors from it, so a caller can write `except PickListError` to catch anything you raise, or name a subclass to handle one case. `raise PickListError` and `raise PickListError("no such SKU")` both work: the bare class is instantiated for you. If a built-in already describes the failure and callers plausibly catch it, inherit from both your base and that built-in so lenient code keeps working.

code

python · 18 lines
python
class PickListError(Exception):
    """Base class for every error this module raises."""


class SkuNotFoundError(PickListError):
    """A requested SKU is not in the warehouse catalogue."""


def pick(sku: str) -> str:
    if sku not in {"A-1", "B-2"}:
        raise SkuNotFoundError(sku)
    return sku


try:
    pick("Z-9")
except PickListError as exc:
    print(type(exc).__name__, exc.args)

go deeper

for a junior

Be ready to type the definition from memory: a class inheriting from Exception, an Error-suffixed name, a docstring body, and a raise. Know that the base class must be Exception rather than BaseException.

for a middle

Explain the mechanics: why the class tree is what callers catch, why a module gets one base class, and what raising a bare class does compared with raising an instance.

for a senior

Show the API judgement — introducing the base class before there are callers, keeping it abstract, and deciding when a failure honestly deserves multiple inheritance from a built-in so existing handlers keep matching.

for a principal

Own the convention across a codebase: whether every package gets its own base, how names are chosen so they survive refactoring, and the migration cost of retrofitting a base class after callers already catch concrete types.

### The rule the interpreter enforces Python only checks one thing about the object in a `raise` statement: it must be an exception class or an exception instance, where "exception" means a subclass of `BaseException`. Everything else about a custom exception — attributes, methods, docstring — is ordinary class machinery. There is no registry to join and no interface to implement: ```python class PickListError(Exception): """Base class for every error this module raises.""" ``` That is a finished, usable exception type. Raising something that is not an exception fails at the `raise` with `TypeError: exceptions must derive from BaseException`; Python has not allowed raising strings since 3.0. ### Exception, not BaseException Derive from `Exception`, not from `BaseException` directly. `BaseException` is the root, and the few types that sit directly under it — the ones signalling interpreter shutdown and interrupt rather than program failure — are deliberately outside what `except Exception` catches. Putting your own error under the root means a caller's honest `except Exception:` misses it, which is almost never what you want. Inherit from `BaseException` only if you are writing a control-flow signal that must survive a broad handler, and expect to justify it. ### One base class per module or package The single most useful design decision here is giving your code its own base: ```python class PickListError(Exception): ... class SkuNotFoundError(PickListError): ... class InventoryTimeoutError(PickListError): ... ``` Now a caller has two levers. `except PickListError` says "anything this module considers a failure" — useful at a boundary where the caller just wants to report and move on. `except SkuNotFoundError` says "this specific case, which I know how to handle". Without a shared base, a caller who wants everything must enumerate your class names in a tuple and update that tuple every time you add one; in practice they write `except Exception` instead, which also swallows the `TypeError` from their own broken code. The base class is what turns a pile of error types into something catchable. By convention the base is never raised directly. It exists as a handle for callers and as the anchor for the subclasses; raising it gives a handler nothing to discriminate on. ### Naming and documenting Name exception classes as nouns ending in `Error` — `SkuNotFoundError`, not `SkuNotFound` or `BadSku`. This matches every built-in (`ValueError`, `KeyError`, `TimeoutError`) and makes the type read correctly at the `except` clause. The class docstring is the natural place to say *when* it is raised, because that sentence is what a caller needs before they can decide to catch it. An empty body is fine: use the docstring as the body rather than `pass`. ### Inheriting from a built-in as well When a failure genuinely *is* an instance of a built-in category, multiple inheritance lets both spellings work: ```python class SkuNotFoundError(PickListError, KeyError): """The requested SKU is not in the catalogue.""" ``` Code that already wrote `except KeyError` around a lookup keeps working, and code that knows your library writes `except PickListError`. The standard library does this itself — `json.JSONDecodeError` is also a `ValueError`, so a caller who only knows "bad input raises ValueError" is still correct. Use it when the built-in's meaning honestly applies; do not bolt on a built-in just to broaden the net, because you then inherit its promises too. ### Class or instance at the raise site `raise SkuNotFoundError` and `raise SkuNotFoundError("Z-9")` are both legal. In the first form Python calls the class with no arguments and raises the resulting instance, so a handler still binds an instance, not the class. Prefer the explicit call whenever you have anything to say, because the arguments become the exception's payload. ### What junior candidates usually miss Three things. First, that inheritance is the *interface*: what a caller can catch is decided entirely by the class tree you chose, so the tree is part of your public API. Second, that the base class should be introduced on day one — retrofitting one later means every existing handler in every caller is now catching the wrong thing. Third, that an exception class is a normal class, so it can carry data; a type whose only content is its name is fine for a first cut, but the moment a handler needs to know *which* SKU failed, that value belongs on the instance rather than buried in a message string.

  • What happens if you raise a class that does not derive from BaseException?
    The `raise` statement itself fails with `TypeError: exceptions must derive from BaseException`. The check happens when the raise executes, not when the class is defined, so a class that is never raised can sit in the codebase unnoticed. Raising a string, an instance of a plain class, or a dict all fail the same way.
  • Why would a library inherit an error from both its own base and a built-in such as KeyError?
    So that callers who already wrote `except KeyError` around the operation keep working while callers who know the library can catch its base class. The standard library does this — `json.JSONDecodeError` is also a `ValueError`. The cost is that you inherit the built-in's meaning too, so only do it when the failure honestly is that kind of failure.
  • Should the module-level base exception class ever be raised directly?
    No. It exists as a catch handle and as the anchor of the tree. Raising it gives a handler nothing to discriminate on, and it usually signals a failure mode nobody bothered to name. Keep it abstract by convention and always raise a subclass.

The class tree is the set of handles you offer callers: one broad handle for "anything from this module", and a labelled one for each case somebody can actually do something about.

saying these in an interview costs you the question

  • Deriving a custom error from BaseException instead of Exception
  • Defining error types with no shared base class
  • Thinking a custom exception needs registration or a metaclass
  • Believing raise MyError requires explicit instantiation
  • Naming exception classes as verbs or without an Error suffix

context

open as a page

EAFP and LBYL in Python: what do the two idioms mean, and which does the language favour?

level: juniorimportance: must knowfreq 70%

basics

~20 s

EAFP means easier to ask forgiveness than permission: attempt the operation and catch the exception it raises. LBYL means look before you leap: check the preconditions with an if first. Python favours EAFP, because a separate guard can drift away from the operation it protects.

open as a page

What is the difference between BaseException and Exception in Python?

level: juniorimportance: must knowfreq 68%

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.

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 you log a caught Python exception with its full traceback rather than just its message?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Inside the except block call logging.exception(msg), or pass exc_info=True to any logging call, so the record carries the active exception and its traceback. traceback.format_exc() returns that same text as a string; logging str(exc) throws the stack away.

open as a page

What order do a Python try statement's except, else and finally clauses run in?

level: juniorimportance: must knowfreq 72%

basics

~20 s

The try body runs first. If it raises, the first matching except clause runs and else is skipped; if it does not raise, else runs. The finally clause runs last on every path, including while an exception is still propagating.

open as a page

Does entering a try block that never raises cost anything in CPython 3.14?

level: middleimportance: must knowfreq 55%

basics

~20 s

Essentially nothing. Since CPython 3.11 the compiler emits no handler-setup instruction for a try block: the guarded ranges are recorded in a static table on the code object and are read only when an exception is actually propagating.

open as a page

Why is checking os.path.exists() before open() weaker than catching FileNotFoundError?

level: middleimportance: must knowfreq 45%

basics

~20 s

The check and the open are two separate system calls, and the filesystem can change in between, so a True answer does not promise the open will succeed. os.path.exists() also answers False for a path you simply cannot stat, and it says nothing about whether the path is a file you can read.

open as a page

How does `raise NewError(...) from err` differ from raising NewError plainly inside an `except` block?

level: middleimportance: must knowfreq 60%

basics

~20 s

The from clause sets the new exception's cause to err and flips suppress_context to True, so the traceback says "direct cause". Raising plainly leaves cause as None and only records the implicit context link, printed as "during handling".

open as a page

What does BaseException.add_note() do to an exception object in Python?

level: juniorimportance: should knowfreq 32%

basics

~20 s

add_note appends a string to the exception instance's notes list without touching its class, args or message. Python 3.11 added it. Each note is printed on its own line just below the traceback's final error line.

open as a page

How do you use timeit to measure the cost of a try/except block?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Write both variants as statement strings, put shared state in a setup string that timeit runs once untimed, call timeit.repeat with a large number of loops, and compare the minimum of the repeats rather than the average.

open as a page

Why does a Python traceback sometimes print two exceptions joined by "During handling of the above exception"?

level: juniorimportance: should knowfreq 45%

basics

~10 s

That line marks implicit chaining. The second exception was raised while the first was still being handled, so Python stored the first one on the new exception's context attribute and printed both, oldest first.

open as a page

How do you use add_note in an except handler to add context before re-raising?

level: middleimportance: should knowfreq 28%

basics

~20 s

Catch the error with except ... as exc, call exc.add_note with what this layer knows, then use a bare raise. The same object keeps propagating with its type, args and traceback intact, now carrying an extra line of context.

open as a page

Why should a custom exception carry structured attributes instead of only a message string?

level: middleimportance: should knowfreq 55%

basics

~10 s

Because a handler cannot act on prose without parsing it. Store the failing values as attributes in init, pass them to super().init so they land in args, and format the human-readable sentence in str.

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

What does `raise ... from None` do to a Python exception's __context__ and to the printed traceback?

level: middleimportance: should knowfreq 35%

basics

~10 s

It sets cause to None and suppress_context to True, so the default traceback prints only the new exception. The original is still attached as context on the object; only the display is suppressed.

open as a page

What does an exception's __traceback__ attribute hold, and how do you walk it?

level: middleimportance: should knowfreq 44%

basics

~20 s

It holds a traceback object, not text: a singly linked list of frames. Each node exposes tb_frame, tb_lineno and tb_lasti, and tb_next points one step deeper toward the raise. Walk it by following tb_next until it is None.

open as a page

How does Python choose which except clause handles a raised exception?

level: middleimportance: should knowfreq 50%

basics

~20 s

Python tests the except clauses top to bottom and runs the first whose named class is the raised exception's class or a base of it. Only that clause runs; a parenthesized tuple matches any of several types.

open as a page

Why does a loop that raises and catches an exception on nearly every iteration get slow?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because the raise itself is the expensive part: each one instantiates the exception, evaluates its message argument, allocates a traceback entry for every frame it unwinds through, and is matched against the except clauses. Entering the try is free; raising is not.

open as a page

How granular should a module's custom exception subclasses be?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Split when a caller would handle the cases differently; otherwise keep one class and put the detail in an attribute. A class per decision, a field per detail: a distinction no handler branches on is noise.

open as a page

`except Exception: continue` wraps each of 6,800 invoice rows in a nightly PDF renderer — what is wrong with that EAFP boundary?

level: seniorimportance: should knowfreq 40%

basics

~20 s

That is not EAFP, it is a swallowed exception. A handler that broad catches genuine bugs alongside bad rows, discards the traceback, and reports a batch where every row failed as a clean success. Narrow the try to the failing call, name the exceptions, log with the traceback, and fail the job when the failures look systemic.

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

How do you walk a Python exception's __cause__ and __context__ to the root failure when only the wrapper is reported?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Follow cause if it is set, otherwise context unless suppress_context is True, and repeat until the link is None. Carry a set of seen object ids so a hand-assigned cause cannot loop forever.

open as a page

A route-optimisation job wraps a failure in a new exception and its logged stack silently truncates to the wrapper; why, and how do you keep the original frames?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A brand-new exception starts with an empty traceback that only grows from the raise site outward, so the original frames are gone from it. Re-raise the same object to keep them, or transplant them with with_traceback, or let chaining carry the original alongside.

open as a page

Inside an except block, what does a bare `raise` statement re-raise?

level: seniorimportance: should knowfreq 44%

basics

~10 s

A bare raise re-raises the exception currently being handled, taken from the interpreter's own exception state rather than from any variable. Outside an active handler it raises RuntimeError: No active exception to reraise.

open as a page

When should a Python API raise a custom exception rather than return a result value?

level: principalimportance: should knowfreq 38%

basics

~10 s

Raise when the failure is outside the caller's expected outcomes and most callers cannot continue; return a value when failure is a routine result every caller inspects anyway. Batch work aggregates failures instead.

open as a page

Why can contextlib.suppress cost more than try/except pass in a hot loop?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Because a with statement pays on every entry: it calls the suppressor's enter and exit, both ordinary Python-level methods, while entering a try block has emitted no handler-setup instruction since CPython 3.11 and costs nothing unless something raises.

open as a page

Why can Python's built-in hasattr() report False for an attribute that is genuinely defined?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

hasattr() is defined as calling getattr() and returning False if AttributeError is raised. So if the attribute is a property or a dynamic lookup whose own code raises AttributeError, the guard reports the attribute as absent and hides the real error inside it.

open as a page

Why is the name in `except ValueError as err` undefined after the except block?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Python 3 compiles the handler as if it ended with del err, so the binding is gone once the block exits — breaking the cycle from the exception through its traceback to the frame. Copy it elsewhere to keep it.

open as a page

Do notes added with add_note survive when an except* handler splits an ExceptionGroup?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

Yes. An except* handler receives a derived group built around the same member exception objects, so notes added to members are still there, and the group's own notes list is copied onto each derived group. Nothing is lost by filtering.

open as a page

showing 1–30 of 31