How do you define a custom exception class in Python, and what should it inherit from?
answer
- Exceptions are just classes
- Inheritance decides what callers can catch
- One base class per module
- Derive from Exception, not the root
- Docstring body is enough
basics
~20 sWrite 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 sA 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 linesclass 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
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.
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.
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.
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