Custom Exceptions & the Hierarchy Interview Questions & Answers
6 questions Updated 2026-06-18
Python interview questions on the exception hierarchy, BaseException vs Exception, defining custom exception classes, passing messages, and how catching a base class catches its subclasses.
All exceptions inherit from BaseException at the root. Directly under it sit a
few special ones — SystemExit, KeyboardInterrupt, and
GeneratorExit — that are not meant to be caught by normal error handling.
Everything you'd normally catch (and every built-in error like ValueError)
descends from Exception, which is itself a child of BaseException.
# BaseException
# ├── SystemExit
# ├── KeyboardInterrupt
# └── Exception <- catch THIS, not BaseException
# ├── ValueError
# ├── KeyError
# └── ...
Catching BaseException (or a bare except:) traps KeyboardInterrupt and
SystemExit, so Ctrl-C and clean shutdowns stop working. Catch Exception
(or narrower) and let the system-level ones propagate.
Subclass Exception (almost never BaseException). The simplest custom
exception needs no body at all — pass is enough, since it inherits message
handling from Exception.
class ConfigError(Exception):
"""Raised when configuration is invalid."""
raise ConfigError("missing 'port' key")
try:
...
except ConfigError as e:
print(e) # missing 'port' key
Give the class a clear, specific name ending in Error, and a docstring describing
when it's raised. Even an empty subclass is valuable because it lets callers catch
your error specifically instead of a generic Exception.
Create a custom exception when callers need to distinguish your error from others and handle it differently. A common pattern is a single base exception for your library/app, with specific subclasses beneath it — so users can catch the base to handle "anything from this library" or a subclass for fine-grained control.
class PaymentError(Exception):
"""Base for all payment problems."""
class CardDeclined(PaymentError): pass
class InsufficientFunds(PaymentError): pass
try:
charge(card)
except InsufficientFunds:
retry_later()
except PaymentError: # catches any other payment error
alert_support()
Don't invent a custom exception when a built-in already fits — bad input is a
ValueError, a missing key is a KeyError. Add your own only when it carries
meaning the built-ins can't.
Arguments passed to an exception are stored in its .args tuple, and the first
one becomes the string returned by str(exception). To attach structured data,
add attributes in a custom __init__ (and call super().__init__() so the message
still works).
raise ValueError("bad value", 42)
# e.args == ("bad value", 42)
class ApiError(Exception):
def __init__(self, message, status_code):
super().__init__(message) # sets the message / .args
self.status_code = status_code
try:
raise ApiError("not found", 404)
except ApiError as e:
print(e, e.status_code) # not found 404
Use a plain message for simple cases; add attributes when handlers need to inspect details (like an HTTP status or the offending value) rather than parse the text.
except matches via isinstance, so a handler for a base class catches the
base and every subclass. That's why except Exception catches almost
everything, and why ordering matters: put specific subclasses before their base,
or the base will intercept them first.
class AppError(Exception): pass
class NotFound(AppError): pass
try:
raise NotFound("user")
except AppError: # matches — NotFound IS-A AppError
print("caught by base")
try:
raise NotFound("user")
except AppError: # this runs first...
print("base")
except NotFound: # ...so this is unreachable!
print("specific")
Order except clauses most-specific first. The same rule is why
except Exception should come last among your handlers.
Knowing the standard ones lets you catch precisely and raise the right error.
ValueError — right type, wrong value (int("abc")). TypeError — wrong
type entirely ("x" + 1). KeyError — missing dict key. IndexError —
list index out of range. AttributeError — missing attribute. KeyError
and IndexError both subclass LookupError.
int("abc") # ValueError
"x" + 1 # TypeError
{"a": 1}["b"] # KeyError
[1, 2][5] # IndexError
None.foo # AttributeError
Raise the built-in that best describes the problem instead of a generic
Exception — ValueError for bad arguments, TypeError for wrong types — so
callers can handle them idiomatically.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.