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 ExceptionValueError 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.