Skip to content
datarekha
Python Easy Asked at ShopifyAsked at TwilioAsked at HubSpot

How do you define and raise custom exceptions in Python?

The short answer

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.

Learn it properly Errors & Exceptions

Keep practising

All Python questions

Explore further