How do you define and raise custom exceptions in Python?
Subclass Exception (not BaseException) to create a custom exception. Give it a clear name, optionally store structured data in __init__, and place it in a dedicated exceptions module. Raise it with raise and catch it with except, narrowing to your specific type before broader ones.
How to think about it
Custom exceptions are really about communication. A bare ValueError tells the caller almost nothing; a SchemaValidationError carrying a .field attribute tells them precisely what broke and hands them something to act on in code. So the question is quietly testing whether you design exceptions the way you’d design an API — clear names, structured data, a hierarchy that mirrors the domain.
Three things an interviewer is listening for: that you subclass Exception (never BaseException, which would also swallow KeyboardInterrupt and SystemExit), that you attach data as attributes rather than cramming it into the message string, and that you chain with raise … from e so the original cause survives in the traceback.
A worked example
# A custom exception is just a subclass — the name carries the meaning
class ValidationError(Exception):
"""Raised when input fails business-rule validation."""
# Attach structured data so callers branch on attributes, not on text
class PaymentError(Exception):
def __init__(self, message, code, amount):
super().__init__(message)
self.code = code
self.amount = amount
try:
raise PaymentError("Card declined", code="insufficient_funds", amount=99.99)
except PaymentError as e:
print(f"Payment failed: {e}")
print(f" code: {e.code}, amount: {e.amount}")
print()
# A hierarchy lets callers catch the whole family with one except
class PipelineError(Exception):
"""Base for every error this package raises."""
class SchemaValidationError(PipelineError):
def __init__(self, field, expected, got):
super().__init__(f"Field '{field}': expected {expected}, got {got}")
self.field = field
try:
raise SchemaValidationError("age", "int", "str")
except PipelineError as e: # catches the base — and all its children
print(f"Pipeline failed: {e}")
if isinstance(e, SchemaValidationError):
print(f" Bad field: {e.field}")
print()
# Chain with 'from e' so the original cause is preserved in the traceback
def parse_user_input(raw):
try:
return int(raw)
except ValueError as e:
raise ValidationError(f"Expected an integer, got: {raw!r}") from e
try:
parse_user_input("abc")
except ValidationError as e:
print(f"Validation error: {e}")
print(f" Caused by: {e.__cause__}")
Payment failed: Card declined
code: insufficient_funds, amount: 99.99
Pipeline failed: Field 'age': expected int, got str
Bad field: age
Validation error: Expected an integer, got: 'abc'
Caused by: invalid literal for int() with base 10: 'abc'
Three moves, each earning its keep: PaymentError lets a caller read e.code instead of parsing a sentence; the PipelineError base lets one except catch an entire library’s failures while isinstance still allows fine-grained handling; and from e keeps the real cause — the int() failure — attached, so the traceback tells the whole story.
The idea underneath
When you write a library or a service, the exceptions you raise are part of the contract you offer callers. They deserve the same care as your function signatures: named precisely, carrying the data a caller needs to recover or log well, and arranged in a hierarchy that matches the domain.