Your banking app needs to signal "insufficient funds". Using generic ValueError loses meaning. A custom InsufficientFundsError carries the balance and requested amount - meaningful error information for the specific domain.

Simple custom exception

Create your own exception type.

basic_custom.py
Replay: real traced execution (multi-file project)
# Define basic custom exception

def main():
    # Simple custom exception
    class InsufficientFundsError(Exception):
        """Raised when account balance is insufficient"""
        pass

    def withdraw(balance, amount):
        if amount > balance:
            raise InsufficientFundsError("Not enough funds")
        return balance - amount

    print("Bank withdrawal:\n")

    # Successful withdrawal
    try:
        new_balance = withdraw(100, 30)
        print(f"  Withdrew $30: Balance ${new_balance}")
    except InsufficientFundsError as e:
        print(f"  Error: {e}")

    # Failed withdrawal
    try:
        new_balance = withdraw(100, 150)
        print(f"  Withdrew $150: Balance ${new_balance}")
    except InsufficientFundsError as e:
        print(f"  Error: {e}")


    # Multiple custom exceptions
    class InvalidUsernameError(Exception):
        pass

    class InvalidEmailError(Exception):
        pass

    class InvalidAgeError(Exception):
        pass

    def validate_user(username, email, age):
        if len(username) < 3:
            raise InvalidUsernameError("Username too short")
        if "@" not in email:
            raise InvalidEmailError("Invalid email format")
        if age < 18:
            raise InvalidAgeError("Must be 18 or older")
        return True

    print("\nUser validation:")

    users = [
        ("alice", "alice@example.com", 25),
        ("ab", "alice@example.com", 25),
        ("alice", "invalid", 25),
        ("alice", "alice@example.com", 15)
    ]

    for username, email, age in users:
        try:
            validate_user(username, email, age)
            print(f"  {username}: Valid")
        except (InvalidUsernameError, InvalidEmailError, InvalidAgeError) as e:
            print(f"  {username}: {type(e).__name__} - {e}")

    # Custom exception with default message
    class ConfigurationError(Exception):
        """Raised when configuration is invalid"""
        def __init__(self, message="Invalid configuration"):
            super().__init__(message)

    try:
        raise ConfigurationError()
    except ConfigurationError as e:
        print(f"\nDefault message: {e}")

    try:
        raise ConfigurationError("Missing API key")
    except ConfigurationError as e:
        print(f"Custom message: {e}")

if __name__ == "__main__":
    main()
  1. def main(): # Simple custom exception

    3def main():4    # Simple custom exception5    class InsufficientFundsError(Exception):6        """Raised when account balance is insufficient"""7        pass8    9    def withdraw(balance, amount):10        if amount > balance:11            raise InsufficientFundsError("Not enough funds")12        return balance - amount13    14    print("Bank withdrawal:\n")
    outputBank withdrawal:
  2. def withdraw(balance, amount):

    pass 1 of 2
    9def withdraw(balance100, amount30):10    if amount > balance:11        raise InsufficientFundsError("Not enough funds")12    return balance100 - amount30
  3. new_balance ← 70

    17try:18    new_balance→ 70 = withdraw(100, 30)19    print(f"  Withdrew $30: Balance ${new_balance70}")20except InsufficientFundsError as e:
    output  Withdrew $30: Balance $70
  4. def withdraw(balance, amount):

    pass 2 of 2
    9def withdraw(balance100, amount150):10    if amount > balance:11        raise InsufficientFundsError("Not enough funds")
  5. if amount > balance:

    9def withdraw(balance, amount):10    if amount150 > balance100:11        raise InsufficientFundsError("Not enough funds")12    return balance - amount
  6. except InsufficientFundsError as e:

    26    print(f"  Withdrew $150: Balance ${new_balance}")27except InsufficientFundsError as e:28    print(f"  Error: {eNot enough funds}")2930#@help h1
    output  Error: Not enough funds
      Error: Not enough funds
  7. users ← [('alice', 'alice@example.com', 25), ('ab', 'alice@example.com', 25), ('alice', 'invalid', 25), ('alice', 'alice@example.com', 15)]

    30#@help h131# Custom exceptions inherit from Exception32# Name them with "Error" suffix by convention33# Use when built-in exceptions aren't specific enough34#@end3536# Multiple custom exceptions37class InvalidUsernameError(Exception):38    pass3940class InvalidEmailError(Exception):41    pass4243class InvalidAgeError(Exception):44    pass4546def validate_user(username, email, age):47    if len(username) < 3:48        raise InvalidUsernameError("Username too short")49    if "@" not in email:50        raise InvalidEmailError("Invalid email format")51    if age < 18:52        raise InvalidAgeError("Must be 18 or older")53    return True5455print("\nUser validation:")5657users→ [('alice', 'alice@example.com', 25), ('ab', 'alice@example.com', 25), ('alice', 'invalid', 25), ('alice', 'alice@example.com', 15)] = [58    ("alice", "alice@example.com", 25),59    ("ab", "alice@example.com", 25),60    ("alice", "invalid", 25),61    ("alice", "alice@example.com", 15)62]
    output
    User validation:
  8. for username, email, age in users:

    pass 1 of 4
    64for usernamealice, emailalice@example.com, age25 in users[('alice', 'alice@example.com', 25), ('ab', 'alice@example.com', 25), ('alice', 'invalid', 25), ('alice', 'alice@example.com', 15)]:65    try:66        validate_user(username, email, age)
    All 4 passes — pass 1 is the card above
    passusernameemailage
    1alicealice@example.com25
    2abalice@example.com25
    3aliceinvalid25
    4alicealice@example.com15
  9. try:

    pass 1 of 4
    64for username, email, age in users:65    try:66        validate_user(usernamealice, emailalice@example.com, age25)67        print(f"  {username}: Valid")
    All 4 passes — pass 1 is the card above
    passusernameemailage
    1alicealice@example.com25
    2abalice@example.com25
    3aliceinvalid25
    4alicealice@example.com15
  10. def validate_user(username, email, age):

    pass 1 of 4
    46def validate_user(usernamealice, emailalice@example.com, age25):47    if len(username) < 3:48        raise InvalidUsernameError("Username too short")49    if "@" not in email:50        raise InvalidEmailError("Invalid email format")51    if age < 18:52        raise InvalidAgeError("Must be 18 or older")53    return True
    All 4 passes — pass 1 is the card above
    passusernameemailage
    1alicealice@example.com25
    2abalice@example.com25
    3aliceinvalid25
    4alicealice@example.com15
  11. validate_user(username, email, age)

    65try:66    validate_user(usernamealice, emailalice@example.com, age25)67    print(f"  {usernamealice}: Valid")68except (InvalidUsernameError, InvalidEmailError, InvalidAgeError) as e:
    output  alice: Valid
  12. if len(username) < 3:

    46def validate_user(username, email, age):47    if len(usernameab) < 3:48        raise InvalidUsernameError("Username too short")49    if "@" not in email:
  13. except (InvalidUsernameError, InvalidEmailError, InvalidAgeError) as e…

    pass 1 of 3
    67    print(f"  {username}: Valid")68except (InvalidUsernameError, InvalidEmailError, InvalidAgeError) as e:69    print(f"  {usernameab}: {type(eUsername too short).__name__} - {e}")
    output  ab: InvalidUsernameError - Username too short
      ab: InvalidUsernameError - Username too short
    All 3 passes — pass 1 is the card above
    passusernameeemailage
    1abUsername too shortinvalid
    2aliceInvalid email format15
    3aliceMust be 18 or older
  14. if "@" not in email:

    48    raise InvalidUsernameError("Username too short")49if "@" not in emailinvalid:50    raise InvalidEmailError("Invalid email format")51if age < 18:
  15. if age < 18:

    50    raise InvalidEmailError("Invalid email format")51if age15 < 18:52    raise InvalidAgeError("Must be 18 or older")53return True
  16. # Custom exception with default message

    71# Custom exception with default message72class ConfigurationError(Exception):73    """Raised when configuration is invalid"""74    def __init__(self, message="Invalid configuration"):75        super().__init__(message)
  17. def __init__(self, message="Invalid configuration"):

    pass 1 of 2
    73"""Raised when configuration is invalid"""74def __init__(self(empty), messageInvalid configuration="Invalid configuration"):75    super().__init__(message)
  18. except ConfigurationError as e:

    78    raise ConfigurationError()79except ConfigurationError as e:80    print(f"\nDefault message: {eInvalid configuration}")8182try:
    output
    Default message: Invalid configuration
    
    Default message: Invalid configuration
  19. def __init__(self, message="Invalid configuration"):

    pass 2 of 2
    73"""Raised when configuration is invalid"""74def __init__(selfMissing API key, messageMissing API key="Invalid configuration"):75    super().__init__(message)
  20. except ConfigurationError as e:

    83    raise ConfigurationError("Missing API key")84except ConfigurationError as e:85    print(f"Custom message: {eMissing API key}")
    outputCustom message: Missing API key
    Custom message: Missing API key
  21. main()

    87if __name__ == "__main__":88    main()

Inherit from Exception. Add pass for minimal exception.

custom exception Your own exception class. Meaningful name, specific to your domain.

Exception with data

Include relevant context in the exception.

with_data.py
Replay: real traced execution (multi-file project)
# Custom exception with additional data

def main():
    # Custom exception with fields
    class ValidationError(Exception):
        def __init__(self, message, field_name, invalid_value):
            super().__init__(message)
            self.field_name = field_name
            self.invalid_value = invalid_value

    def validate_age(age):
        if not isinstance(age, int):
            raise ValidationError(
                "Age must be an integer",
                field_name="age",
                invalid_value=age
            )
        if age < 0:
            raise ValidationError(
                "Age cannot be negative",
                field_name="age",
                invalid_value=age
            )
        if age > 150:
            raise ValidationError(
                "Age is unrealistic",
                field_name="age",
                invalid_value=age
            )
        return True

    print("Age validation with context:\n")

    test_ages = [25, "invalid", -5, 200]

    for age in test_ages:
        try:
            validate_age(age)
            print(f"  {age}: Valid")
        except ValidationError as e:
            print(f"  Field: {e.field_name}")
            print(f"  Value: {e.invalid_value}")
            print(f"  Error: {e}")
            print()


    # Exception with error code
    class DatabaseError(Exception):
        def __init__(self, message, query, error_code):
            super().__init__(message)
            self.query = query
            self.error_code = error_code

    def execute_query(query):
        if "DROP" in query:
            raise DatabaseError(
                "DROP not allowed",
                query=query,
                error_code=403
            )
        if "invalid" in query:
            raise DatabaseError(
                "Syntax error",
                query=query,
                error_code=400
            )
        return "SUCCESS"

    print("\nDatabase queries:")

    queries = [
        "SELECT * FROM users",
        "DROP TABLE users",
        "invalid syntax"
    ]

    for query in queries:
        try:
            result = execute_query(query)
            print(f"  '{query}': {result}")
        except DatabaseError as e:
            print(f"  Error code: {e.error_code}")
            print(f"  Query: {e.query}")
            print(f"  Message: {e}")
            print()

    # Exception with multiple context fields
    class PaymentError(Exception):
        def __init__(self, message, amount, account_id, transaction_id):
            super().__init__(message)
            self.amount = amount
            self.account_id = account_id
            self.transaction_id = transaction_id

        def __str__(self):
            return (f"{super().__str__()} "
                   f"[Txn:{self.transaction_id}, "
                   f"Account:{self.account_id}, "
                   f"Amount:${self.amount}]")

    try:
        raise PaymentError(
            "Insufficient funds",
            amount=100.00,
            account_id="ACC123",
            transaction_id="TXN456"
        )
    except PaymentError as e:
        print("Payment failure:")
        print(f"  {e}")
        print(f"  Amount: ${e.amount}")
        print(f"  Account: {e.account_id}")
        print(f"  Transaction: {e.transaction_id}")

if __name__ == "__main__":
    main()
  1. test_ages ← [25, 'invalid', -5, 200]

    3def main():4    # Custom exception with fields5    class ValidationError(Exception):6        def __init__(self, message, field_name, invalid_value):7            super().__init__(message)8            self.field_name = field_name9            self.invalid_value = invalid_value10    11    def validate_age(age):12        if not isinstance(age, int):13            raise ValidationError(14                "Age must be an integer",15                field_name="age",16                invalid_value=age17            )18        if age < 0:19            raise ValidationError(20                "Age cannot be negative",21                field_name="age",22                invalid_value=age23            )24        if age > 150:25            raise ValidationError(26                "Age is unrealistic",27                field_name="age",28                invalid_value=age29            )30        return True31    32    print("Age validation with context:\n")33    34    test_ages→ [25, 'invalid', -5, 200] = [25, "invalid", -5, 200]
    outputAge validation with context:
  2. for age in test_ages:

    pass 1 of 4
    36for age25 in test_ages[25, 'invalid', -5, 200]:37    try:38        validate_age(age)
    All 4 passes — pass 1 is the card above
    passage
    125
    2invalid
    3-5
    4200
  3. try:

    pass 1 of 4
    36for age in test_ages:37    try:38        validate_age(age25)39        print(f"  {age}: Valid")
    All 4 passes — pass 1 is the card above
    passage
    125
    2invalid
    3-5
    4200
  4. def validate_age(age):

    pass 1 of 4
    11def validate_age(age25):12    if not isinstance(age, int):13        raise ValidationError(14            "Age must be an integer",15            field_name="age",16            invalid_value=age17        )18    if age < 0:19        raise ValidationError(20            "Age cannot be negative",21            field_name="age",22            invalid_value=age23        )24    if age > 150:25        raise ValidationError(26            "Age is unrealistic",27            field_name="age",28            invalid_value=age29        )30    return True
    All 4 passes — pass 1 is the card above
    passage
    125
    2invalid
    3-5
    4200
  5. validate_age(age)

    37try:38    validate_age(age25)39    print(f"  {age25}: Valid")40except ValidationError as e:
    output  25: Valid
  6. if not isinstance(age, int):

    11def validate_age(age):12    if not isinstance(ageinvalid, int):13        raise ValidationError(14            "Age must be an integer",15            field_name="age",16            invalid_value=ageinvalid17        )18    if age < 0:
  7. self.field_name ← age, self.invalid_value ← invalid

    pass 1 of 3
    5class ValidationError(Exception):6    def __init__(selfAge must be an integer, messageAge must be an integer, field_nameage, invalid_valueinvalid):7        super().__init__(message)8        self.field_name→ age = field_nameage9        self.invalid_value→ invalid = invalid_valueinvalid
    All 3 passes — pass 1 is the card above
    passselfmessageinvalid_valueageself.field_nameself.invalid_value
    1Age must be an integerAge must be an integerinvalid-5ageinvalid
    2Age cannot be negativeAge cannot be negative-5200age-5
    3Age is unrealisticAge is unrealistic200age200
  8. except ValidationError as e:

    pass 1 of 3
    39    print(f"  {age}: Valid")40except ValidationError as e:41    print(f"  Field: {e.field_nameage}")42    print(f"  Value: {e.invalid_valueinvalid}")43    print(f"  Error: {eAge must be an integer}")44    print()
    output  Field: age
      Value: invalid
      Error: Age must be an integer
    All 3 passes — pass 1 is the card above
    passe.invalid_valueeage
    1invalidAge must be an integer-5
    2-5Age cannot be negative200
    3200Age is unrealistic
  9. if age < 0:

    17    )18if age-5 < 0:19    raise ValidationError(20        "Age cannot be negative",21        field_name="age",22        invalid_value=age-523    )24if age > 150:
  10. if age > 150:

    23    )24if age200 > 150:25    raise ValidationError(26        "Age is unrealistic",27        field_name="age",28        invalid_value=age20029    )30return True
  11. queries ← ['SELECT * FROM users', 'DROP TABLE users', 'invalid syntax']

    46#@help h147# Add fields to exception __init__48# Store context: field names, values, error codes49# Access fields when catching exception50#@end5152# Exception with error code53class DatabaseError(Exception):54    def __init__(self, message, query, error_code):55        super().__init__(message)56        self.query = query57        self.error_code = error_code5859def execute_query(query):60    if "DROP" in query:61        raise DatabaseError(62            "DROP not allowed",63            query=query,64            error_code=40365        )66    if "invalid" in query:67        raise DatabaseError(68            "Syntax error",69            query=query,70            error_code=40071        )72    return "SUCCESS"7374print("\nDatabase queries:")7576queries→ ['SELECT * FROM users', 'DROP TABLE users', 'invalid syntax'] = [77    "SELECT * FROM users",78    "DROP TABLE users",79    "invalid syntax"80]
    output
    Database queries:
  12. for query in queries:

    pass 1 of 3
    82for querySELECT * FROM users in queries['SELECT * FROM users', 'DROP TABLE users', 'invalid syntax']:83    try:84        result = execute_query(query)
    All 3 passes — pass 1 is the card above
    passqueryselfmessageerror_codee.error_codee.queryeself.queryself.error_code
    1SELECT * FROM users
    2DROP TABLE usersDROP not allowedDROP not allowed403403DROP TABLE usersDROP not allowedDROP TABLE users403
    3invalid syntaxSyntax errorSyntax error400400invalid syntaxSyntax errorinvalid syntax400
  13. try:

    pass 1 of 3
    82for query in queries:83    try:84        result = execute_query(querySELECT * FROM users)85        print(f"  '{query}': {result}")
    All 3 passes — pass 1 is the card above
    passqueryselfmessageerror_codee.error_codee.queryeself.queryself.error_code
    1SELECT * FROM users
    2DROP TABLE usersDROP not allowedDROP not allowed403403DROP TABLE usersDROP not allowedDROP TABLE users403
    3invalid syntaxSyntax errorSyntax error400400invalid syntaxSyntax errorinvalid syntax400
  14. def execute_query(query):

    pass 1 of 3
    59def execute_query(querySELECT * FROM users):60    if "DROP" in query:61        raise DatabaseError(62            "DROP not allowed",63            query=query,64            error_code=40365        )66    if "invalid" in query:67        raise DatabaseError(68            "Syntax error",69            query=query,70            error_code=40071        )72    return "SUCCESS"
    All 3 passes — pass 1 is the card above
    passqueryselfmessageerror_codee.error_codee.queryeself.queryself.error_code
    1SELECT * FROM users
    2DROP TABLE usersDROP not allowedDROP not allowed403403DROP TABLE usersDROP not allowedDROP TABLE users403
    3invalid syntaxSyntax errorSyntax error400400invalid syntaxSyntax errorinvalid syntax400
  15. result ← SUCCESS

    83try:84    result→ SUCCESS = execute_query(querySELECT * FROM users)85    print(f"  '{querySELECT * FROM users}': {resultSUCCESS}")86except DatabaseError as e:
    output  'SELECT * FROM users': SUCCESS
  16. if "DROP" in query:

    59def execute_query(query):60    if "DROP" in queryDROP TABLE users:61        raise DatabaseError(62            "DROP not allowed",63            query=queryDROP TABLE users,64            error_code=40365        )66    if "invalid" in query:
  17. self.query ← DROP TABLE users, self.error_code ← 403

    pass 1 of 2
    53class DatabaseError(Exception):54    def __init__(selfDROP not allowed, messageDROP not allowed, queryDROP TABLE users, error_code403):55        super().__init__(message)56        self.query→ DROP TABLE users = queryDROP TABLE users57        self.error_code→ 403 = error_code403
  18. except DatabaseError as e:

    pass 1 of 2
    85    print(f"  '{query}': {result}")86except DatabaseError as e:87    print(f"  Error code: {e.error_code403}")88    print(f"  Query: {e.queryDROP TABLE users}")89    print(f"  Message: {eDROP not allowed}")90    print()
    output  Error code: 403
      Query: DROP TABLE users
      Message: DROP not allowed
  19. if "invalid" in query:

    65    )66if "invalid" in queryinvalid syntax:67    raise DatabaseError(68        "Syntax error",69        query=queryinvalid syntax,70        error_code=40071    )72return "SUCCESS"
  20. self.query ← invalid syntax, self.error_code ← 400

    pass 2 of 2
    53class DatabaseError(Exception):54    def __init__(selfSyntax error, messageSyntax error, queryinvalid syntax, error_code400):55        super().__init__(message)56        self.query→ invalid syntax = queryinvalid syntax57        self.error_code→ 400 = error_code400
  21. except DatabaseError as e:

    pass 2 of 2
    85    print(f"  '{query}': {result}")86except DatabaseError as e:87    print(f"  Error code: {e.error_code400}")88    print(f"  Query: {e.queryinvalid syntax}")89    print(f"  Message: {eSyntax error}")90    print()
    output  Error code: 400
      Query: invalid syntax
      Message: Syntax error
  22. # Exception with multiple context fields

    92# Exception with multiple context fields93class PaymentError(Exception):94    def __init__(self, message, amount, account_id, transaction_id):95        super().__init__(message)96        self.amount = amount97        self.account_id = account_id98        self.transaction_id = transaction_id99    100    def __str__(self):101        return (f"{super().__str__()} "102               f"[Txn:{self.transaction_id}, "103               f"Account:{self.account_id}, "104               f"Amount:${self.amount}]")
  23. self.amount ← 100.0, self.account_id ← ACC123, self.transaction_id ← TXN456

    93class PaymentError(Exception):94    def __init__(self(empty), messageInsufficient funds, amount100.0, account_idACC123, transaction_idTXN456):95        super().__init__(message)96        self.amount→ 100.0 = amount100.097        self.account_id→ ACC123 = account_idACC12398        self.transaction_id→ TXN456 = transaction_idTXN456
  24. except PaymentError as e:

    112    )113except PaymentError as e:114    print("Payment failure:")115    print(f"  {eInsufficient funds [Txn:TXN456, Account:ACC123, Amount:$100.0]}")116    print(f"  Amount: ${e.amount100.0}")117    print(f"  Account: {e.account_idACC123}")118    print(f"  Transaction: {e.transaction_idTXN456}")
    outputPayment failure:
      Insufficient funds [Txn:TXN456, Account:ACC123, Amount:$100.0]
      Amount: $100.0
      Account: ACC123
      Transaction: TXN456
      Transaction: TXN456
  25. main()

    120if __name__ == "__main__":121    main()

Add __init__ with parameters. Store data as attributes.

Exception hierarchy

Organize related exceptions into a hierarchy.

hierarchy.py
Replay: real traced execution (multi-file project)
# Exception hierarchy

def main():
    # Create exception hierarchy
    class AppError(Exception):
        """Base exception for this application"""
        pass

    class ValidationError(AppError):
        """Validation failed"""
        pass

    class AuthenticationError(AppError):
        """Authentication failed"""
        pass

    class AuthorizationError(AppError):
        """Authorization failed"""
        pass

    class InvalidCredentialsError(AuthenticationError):
        """Specific auth error"""
        pass

    def process_request(username, password, action):
        # Authentication
        if not username or not password:
            raise InvalidCredentialsError("Missing credentials")
        if password != "secret":
            raise AuthenticationError("Wrong password")

        # Authorization
        if action == "delete" and username != "admin":
            raise AuthorizationError(f"{username} cannot delete")

        # Validation
        if not action:
            raise ValidationError("Action required")

        return "SUCCESS"

    print("Request processing:\n")

    requests = [
        ("alice", "secret", "read"),
        ("", "secret", "read"),
        ("bob", "wrong", "read"),
        ("alice", "secret", "delete"),
        ("admin", "secret", "delete")
    ]

    for user, pwd, action in requests:
        try:
            result = process_request(user, pwd, action)
            print(f"  {user} - {action}: {result}")
        except InvalidCredentialsError as e:
            print(f"  {user} - {action}: Invalid credentials")
        except AuthenticationError as e:
            print(f"  {user} - {action}: Auth failed")
        except AuthorizationError as e:
            print(f"  {user} - {action}: Not authorized")
        except ValidationError as e:
            print(f"  {user} - {action}: Validation error")


    # Catch at different levels
    print("\nCatching at different levels:")

    try:
        raise InvalidCredentialsError("Bad login")
    except AuthenticationError as e:
        # Catches InvalidCredentialsError (subclass)
        print(f"  Caught as AuthenticationError: {type(e).__name__}")

    try:
        raise AuthorizationError("No permission")
    except AppError as e:
        # Catches any AppError subclass
        print(f"  Caught as AppError: {type(e).__name__}")

    # Module-specific hierarchy
    class DataError(Exception):
        """Base for data-related errors"""
        pass

    class ParseError(DataError):
        """Failed to parse data"""
        pass

    class FormatError(DataError):
        """Invalid data format"""
        pass

    class EncodingError(DataError):
        """Encoding problem"""
        pass

    def process_data(data, format_type):
        if format_type not in ["json", "xml", "csv"]:
            raise FormatError(f"Unsupported format: {format_type}")
        if not data:
            raise ParseError("Empty data")
        if not data.startswith("{"):
            raise EncodingError("Invalid encoding")
        return "PARSED"

    print("\nData processing:")

    datasets = [
        ('{"name": "Alice"}', "json"),
        ("", "json"),
        ("not json", "json"),
        ('{"name": "Bob"}', "yaml")
    ]

    for data, fmt in datasets:
        try:
            result = process_data(data, fmt)
            print(f"  {fmt}: {result}")
        except ParseError as e:
            print(f"  {fmt}: Parse error - {e}")
        except FormatError as e:
            print(f"  {fmt}: Format error - {e}")
        except EncodingError as e:
            print(f"  {fmt}: Encoding error - {e}")
        except DataError as e:
            # Catch any other DataError
            print(f"  {fmt}: Data error - {e}")

if __name__ == "__main__":
    main()
  1. requests ← [('alice', 'secret', 'read'), ('', 'secret', 'read'), ('bob', 'wrong', 'read'), ('alice', 'secret', 'delete'), ('admin', 'secret', 'delete')]

    3def main():4    # Create exception hierarchy5    class AppError(Exception):6        """Base exception for this application"""7        pass8    9    class ValidationError(AppError):10        """Validation failed"""11        pass12    13    class AuthenticationError(AppError):14        """Authentication failed"""15        pass16    17    class AuthorizationError(AppError):18        """Authorization failed"""19        pass20    21    class InvalidCredentialsError(AuthenticationError):22        """Specific auth error"""23        pass24    25    def process_request(username, password, action):26        # Authentication27        if not username or not password:28            raise InvalidCredentialsError("Missing credentials")29        if password != "secret":30            raise AuthenticationError("Wrong password")31        32        # Authorization33        if action == "delete" and username != "admin":34            raise AuthorizationError(f"{username} cannot delete")35        36        # Validation37        if not action:38            raise ValidationError("Action required")39        40        return "SUCCESS"41    42    print("Request processing:\n")43    44    requests→ [('alice', 'secret', 'read'), ('', 'secret', 'read'), ('bob', 'wrong', 'read'), ('alice', 'secret', 'delete'), ('admin', 'secret', 'delete')] = [45        ("alice", "secret", "read"),46        ("", "secret", "read"),47        ("bob", "wrong", "read"),48        ("alice", "secret", "delete"),49        ("admin", "secret", "delete")50    ]
    outputRequest processing:
  2. for user, pwd, action in requests:

    pass 1 of 5
    52for useralice, pwdsecret, actionread in requests[('alice', 'secret', 'read'), ('', 'secret', 'read'), ('bob', 'wrong', 'read'), ('alice', 'secret', 'delete'), ('admin', 'secret', 'delete')]:53    try:54        result = process_request(user, pwd, action)
    All 5 passes — pass 1 is the card above
    passuserpwdactionusernamepassword
    1alicesecretread
    2(empty)secretread(empty)secret
    3bobwrongreadwrong
    4alicesecretdeletealice
    5adminsecretdelete
  3. try:

    pass 1 of 5
    52for user, pwd, action in requests:53    try:54        result = process_request(useralice, pwdsecret, actionread)55        print(f"  {user} - {action}: {result}")
    All 5 passes — pass 1 is the card above
    passuserpwdactionusernamepassword
    1alicesecretread
    2(empty)secretread(empty)secret
    3bobwrongreadwrong
    4alicesecretdeletealice
    5adminsecretdelete
  4. def process_request(username, password, action): # Authenticat…

    pass 1 of 5
    25def process_request(usernamealice, passwordsecret, actionread):26    # Authentication27    if not username or not password:28        raise InvalidCredentialsError("Missing credentials")29    if password != "secret":30        raise AuthenticationError("Wrong password")31    32    # Authorization33    if action == "delete" and username != "admin":34        raise AuthorizationError(f"{username} cannot delete")35    36    # Validation37    if not action:38        raise ValidationError("Action required")39    40    return "SUCCESS"
    All 5 passes — pass 1 is the card above
    passusernamepasswordactionuser
    1alicesecretread
    2(empty)secretread(empty)
    3bobwrongreadbob
    4alicesecretdeletealice
    5adminsecretdelete
  5. result ← SUCCESS

    53try:54    result→ SUCCESS = process_request(useralice, pwdsecret, actionread)55    print(f"  {useralice} - {actionread}: {resultSUCCESS}")56except InvalidCredentialsError as e:
    output  alice - read: SUCCESS
  6. if not username or not password:

    26# Authentication27if not username(empty) or not passwordsecret:28    raise InvalidCredentialsError("Missing credentials")29if password != "secret":
  7. except InvalidCredentialsError as e:

    55    print(f"  {user} - {action}: {result}")56except InvalidCredentialsError as e:57    print(f"  {user(empty)} - {actionread}: Invalid credentials")58except AuthenticationError as e:
    output   - read: Invalid credentials
  8. if password != "secret":

    28    raise InvalidCredentialsError("Missing credentials")29if passwordwrong != "secret":30    raise AuthenticationError("Wrong password")
  9. except AuthenticationError as e:

    57    print(f"  {user} - {action}: Invalid credentials")58except AuthenticationError as e:59    print(f"  {userbob} - {actionread}: Auth failed")60except AuthorizationError as e:
    output  bob - read: Auth failed
  10. if action == "delete" and username != "admin":

    32# Authorization33if actiondelete == "delete" and usernamealice != "admin":34    raise AuthorizationError(f"{usernamealice} cannot delete")
  11. except AuthorizationError as e:

    59    print(f"  {user} - {action}: Auth failed")60except AuthorizationError as e:61    print(f"  {useralice} - {actiondelete}: Not authorized")62except ValidationError as e:
    output  alice - delete: Not authorized
  12. result ← SUCCESS

    53try:54    result→ SUCCESS = process_request(useradmin, pwdsecret, actiondelete)55    print(f"  {useradmin} - {actiondelete}: {resultSUCCESS}")56except InvalidCredentialsError as e:
    output  admin - delete: SUCCESS
  13. print(" Catching at different levels:")

    72# Catch at different levels73print("\nCatching at different levels:")
    output
    Catching at different levels:
  14. except AuthenticationError as e: # Catches InvalidCredentialsE…

    76    raise InvalidCredentialsError("Bad login")77except AuthenticationError as e:78    # Catches InvalidCredentialsError (subclass)79    print(f"  Caught as AuthenticationError: {type(eBad login).__name__}")8081try:
    output  Caught as AuthenticationError: InvalidCredentialsError
      Caught as AuthenticationError: InvalidCredentialsError
  15. except AppError as e: # Catches any AppError subclass

    82    raise AuthorizationError("No permission")83except AppError as e:84    # Catches any AppError subclass85    print(f"  Caught as AppError: {type(eNo permission).__name__}")8687# Module-specific hierarchy
    output  Caught as AppError: AuthorizationError
      Caught as AppError: AuthorizationError
  16. datasets ← [('{"name": "Alice"}', 'json'), ('', 'json'), ('not json', 'json'), ('{"name": "Bob"}', 'yaml')]

    87# Module-specific hierarchy88class DataError(Exception):89    """Base for data-related errors"""90    pass9192class ParseError(DataError):93    """Failed to parse data"""94    pass9596class FormatError(DataError):97    """Invalid data format"""98    pass99100class EncodingError(DataError):101    """Encoding problem"""102    pass103104def process_data(data, format_type):105    if format_type not in ["json", "xml", "csv"]:106        raise FormatError(f"Unsupported format: {format_type}")107    if not data:108        raise ParseError("Empty data")109    if not data.startswith("{"):110        raise EncodingError("Invalid encoding")111    return "PARSED"112113print("\nData processing:")114115datasets→ [('{"name": "Alice"}', 'json'), ('', 'json'), ('not json', 'json'), ('{"name": "Bob"}', 'yaml')] = [116    ('{"name": "Alice"}', "json"),117    ("", "json"),118    ("not json", "json"),119    ('{"name": "Bob"}', "yaml")120]
    output
    Data processing:
  17. for data, fmt in datasets:

    pass 1 of 4
    122for data{"name": "Alice"}, fmtjson in datasets[('{"name": "Alice"}', 'json'), ('', 'json'), ('not json', 'json'), ('{"name": "Bob"}', 'yaml')]:123    try:124        result = process_data(data, fmt)
    All 4 passes — pass 1 is the card above
    passdatafmteformat_type
    1{"name": "Alice"}json
    2(empty)jsonEmpty data
    3not jsonjsonInvalid encoding
    4{"name": "Bob"}yamlUnsupported format: yamlyaml
  18. try:

    pass 1 of 4
    122for data, fmt in datasets:123    try:124        result = process_data(data{"name": "Alice"}, fmtjson)125        print(f"  {fmt}: {result}")
    All 4 passes — pass 1 is the card above
    passdatafmteformat_type
    1{"name": "Alice"}json
    2(empty)jsonEmpty data
    3not jsonjsonInvalid encoding
    4{"name": "Bob"}yamlUnsupported format: yamlyaml
  19. def process_data(data, format_type):

    pass 1 of 4
    104def process_data(data{"name": "Alice"}, format_typejson):105    if format_type not in ["json", "xml", "csv"]:106        raise FormatError(f"Unsupported format: {format_type}")107    if not data:108        raise ParseError("Empty data")109    if not data.startswith("{"):110        raise EncodingError("Invalid encoding")111    return "PARSED"
    All 4 passes — pass 1 is the card above
    passdataformat_typefmte
    1{"name": "Alice"}json
    2(empty)jsonjsonEmpty data
    3not jsonjsonjsonInvalid encoding
    4{"name": "Bob"}yamlyamlUnsupported format: yaml
  20. result ← PARSED

    123try:124    result→ PARSED = process_data(data{"name": "Alice"}, fmtjson)125    print(f"  {fmtjson}: {resultPARSED}")126except ParseError as e:
    output  json: PARSED
  21. if not data:

    106    raise FormatError(f"Unsupported format: {format_type}")107if not data(empty):108    raise ParseError("Empty data")109if not data.startswith("{"):
  22. except ParseError as e:

    125    print(f"  {fmt}: {result}")126except ParseError as e:127    print(f"  {fmtjson}: Parse error - {eEmpty data}")128except FormatError as e:
    output  json: Parse error - Empty data
  23. if not data.startswith("{"):

    108    raise ParseError("Empty data")109if not datanot json.startswith("{"):110    raise EncodingError("Invalid encoding")111return "PARSED"
  24. except EncodingError as e:

    129    print(f"  {fmt}: Format error - {e}")130except EncodingError as e:131    print(f"  {fmtjson}: Encoding error - {eInvalid encoding}")132except DataError as e:
    output  json: Encoding error - Invalid encoding
  25. if format_type not in ["json", "xml", "csv"]:

    104def process_data(data, format_type):105    if format_typeyaml not in ["json", "xml", "csv"]:106        raise FormatError(f"Unsupported format: {format_typeyaml}")107    if not data:
  26. except FormatError as e:

    127    print(f"  {fmt}: Parse error - {e}")128except FormatError as e:129    print(f"  {fmtyaml}: Format error - {eUnsupported format: yaml}")130except EncodingError as e:
    output  yaml: Format error - Unsupported format: yaml
  27. main()

    136if __name__ == "__main__":137    main()

Base exception for module. Specialized subclasses for specific errors.

Override __str__

Customize exception's string representation.

override_str.py
Replay: real traced execution (multi-file project)
# Override __str__ for custom formatting

def main():
    # Custom __str__ method
    class ProductNotFoundError(Exception):
        def __init__(self, product_id, category=None):
            self.product_id = product_id
            self.category = category

        def __str__(self):
            msg = f"Product {self.product_id} not found"
            if self.category:
                msg += f" in category '{self.category}'"
            return msg

    print("Product lookup:\n")

    try:
        raise ProductNotFoundError(123, "Electronics")
    except ProductNotFoundError as e:
        print(f"  Error: {e}")
        print(f"  Product ID: {e.product_id}")
        print(f"  Category: {e.category}")

    try:
        raise ProductNotFoundError(456)
    except ProductNotFoundError as e:
        print(f"\n  Error: {e}")
        print(f"  Product ID: {e.product_id}")


    # Rich error messages
    class OrderError(Exception):
        def __init__(self, order_id, items, total, reason):
            self.order_id = order_id
            self.items = items
            self.total = total
            self.reason = reason

        def __str__(self):
            return (f"Order #{self.order_id} failed: {self.reason}\n"
                   f"  Items: {self.items}\n"
                   f"  Total: ${self.total:.2f}")

    print("\nOrder processing:")

    try:
        raise OrderError(
            order_id=789,
            items=["Laptop", "Mouse"],
            total=1050.00,
            reason="Payment declined"
        )
    except OrderError as e:
        print(f"{e}")

    # Exception with repr
    class ApiError(Exception):
        def __init__(self, status_code, endpoint, message):
            self.status_code = status_code
            self.endpoint = endpoint
            self.message = message

        def __str__(self):
            return f"[{self.status_code}] {self.message}"

        def __repr__(self):
            return (f"ApiError(status_code={self.status_code}, "
                   f"endpoint='{self.endpoint}', "
                   f"message='{self.message}')")

    error = ApiError(404, "/api/users/123", "User not found")

    print("\nAPI error:")
    print(f"  str: {str(error)}")
    print(f"  repr: {repr(error)}")

    # Format with multiple fields
    class ValidationError(Exception):
        def __init__(self, errors):
            self.errors = errors

        def __str__(self):
            if len(self.errors) == 1:
                field, msg = next(iter(self.errors.items()))
                return f"Validation failed: {field} - {msg}"

            lines = ["Multiple validation errors:"]
            for field, msg in self.errors.items():
                lines.append(f"  - {field}: {msg}")
            return "\n".join(lines)

    print("\nValidation errors:")

    try:
        raise ValidationError({"age": "Must be positive"})
    except ValidationError as e:
        print(f"{e}\n")

    try:
        raise ValidationError({
            "username": "Too short",
            "email": "Invalid format",
            "age": "Must be 18+"
        })
    except ValidationError as e:
        print(f"{e}")

    # Exception with timestamp
    from datetime import datetime

    class TimestampedError(Exception):
        def __init__(self, message):
            self.message = message
            self.timestamp = datetime(2025, 1, 15, 10, 30)

        def __str__(self):
            time_str = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
            return f"[{time_str}] {self.message}"

    print("\nTimestamped error:")

    try:
        raise TimestampedError("Connection lost")
    except TimestampedError as e:
        print(f"  {e}")
        print(f"  Time: {e.timestamp}")

if __name__ == "__main__":
    main()
  1. def main(): # Custom __str__ method

    3def main():4    # Custom __str__ method5    class ProductNotFoundError(Exception):6        def __init__(self, product_id, category=None):7            self.product_id = product_id8            self.category = category9        10        def __str__(self):11            msg = f"Product {self.product_id} not found"12            if self.category:13                msg += f" in category '{self.category}'"14            return msg15    16    print("Product lookup:\n")
    outputProduct lookup:
  2. self.product_id ← 123, self.category ← Electronics

    pass 1 of 2
    5class ProductNotFoundError(Exception):6    def __init__(self(empty), product_id123, categoryElectronics=NoneNone):7        self.product_id→ 123 = product_id1238        self.category→ Electronics = categoryElectronics
  3. except ProductNotFoundError as e:

    19    raise ProductNotFoundError(123, "Electronics")20except ProductNotFoundError as e:21    print(f"  Error: {eProduct 123 not found in category 'Electronics'}")22    print(f"  Product ID: {e.product_id123}")23    print(f"  Category: {e.categoryElectronics}")2425try:
    output  Error: Product 123 not found in category 'Electronics'
      Product ID: 123
      Category: Electronics
      Category: Electronics
  4. self.product_id ← 456, self.category ← None

    pass 2 of 2
    5class ProductNotFoundError(Exception):6    def __init__(self(empty), product_id456, categoryNone=NoneNone):7        self.product_id→ 456 = product_id4568        self.category→ None = categoryNone
  5. except ProductNotFoundError as e:

    26    raise ProductNotFoundError(456)27except ProductNotFoundError as e:28    print(f"\n  Error: {eProduct 456 not found}")29    print(f"  Product ID: {e.product_id456}")3031#@help h1
    output
      Error: Product 456 not found
      Product ID: 456
      Product ID: 456
  6. #@help h1

    31#@help h132# Override __str__() to format exception message33# Called when exception is converted to string34# Can build message from stored fields35#@end3637# Rich error messages38class OrderError(Exception):39    def __init__(self, order_id, items, total, reason):40        self.order_id = order_id41        self.items = items42        self.total = total43        self.reason = reason44    45    def __str__(self):46        return (f"Order #{self.order_id} failed: {self.reason}\n"47               f"  Items: {self.items}\n"48               f"  Total: ${self.total:.2f}")4950print("\nOrder processing:")
    output
    Order processing:
  7. self.order_id ← 789, self.items ← ['Laptop', 'Mouse'], self.total ← 1050.0

    38class OrderError(Exception):39    def __init__(self(empty), order_id789, items['Laptop', 'Mouse'], total1050.0, reasonPayment declined):40        self.order_id→ 789 = order_id78941        self.items→ ['Laptop', 'Mouse'] = items['Laptop', 'Mouse']42        self.total→ 1050.0 = total1050.043        self.reason→ Payment declined = reasonPayment declined
  8. except OrderError as e:

    58    )59except OrderError as e:60    print(f"{eOrder #789 failed: Payment declined
      Items: ['Laptop', 'Mouse']
      Total: $1050.00}")6162# Exception with repr
    outputOrder #789 failed: Payment declined
      Items: ['Laptop', 'Mouse']
      Total: $1050.00
    Order #789 failed: Payment declined
      Items: ['Laptop', 'Mouse']
      Total: $1050.00
  9. # Exception with repr

    62# Exception with repr63class ApiError(Exception):64    def __init__(self, status_code, endpoint, message):65        self.status_code = status_code66        self.endpoint = endpoint67        self.message = message68    69    def __str__(self):70        return f"[{self.status_code}] {self.message}"71    72    def __repr__(self):73        return (f"ApiError(status_code={self.status_code}, "74               f"endpoint='{self.endpoint}', "75               f"message='{self.message}')")7677error = ApiError(404, "/api/users/123", "User not found")
  10. self.status_code ← 404, self.endpoint ← /api/users/123, self.message ← User not found

    63class ApiError(Exception):64    def __init__(self(empty), status_code404, endpoint/api/users/123, messageUser not found):65        self.status_code→ 404 = status_code40466        self.endpoint→ /api/users/123 = endpoint/api/users/12367        self.message→ User not found = messageUser not found
  11. error ← [404] User not found

    77error→ [404] User not found = ApiError(404, "/api/users/123", "User not found")7879print("\nAPI error:")80print(f"  str: {str(error[404] User not found)}")81print(f"  repr: {repr(error[404] User not found)}")8283# Format with multiple fields84class ValidationError(Exception):85    def __init__(self, errors):86        self.errors = errors87    88    def __str__(self):89        if len(self.errors) == 1:90            field, msg = next(iter(self.errors.items()))91            return f"Validation failed: {field} - {msg}"92        93        lines = ["Multiple validation errors:"]94        for field, msg in self.errors.items():95            lines.append(f"  - {field}: {msg}")96        return "\n".join(lines)9798print("\nValidation errors:")
    output
    API error:
      str: [404] User not found
      repr: ApiError(status_code=404, endpoint='/api/users/123', message='User not found')
    
    Validation errors:
  12. self.errors ← {'age': 'Must be positive'}

    pass 1 of 2
    84class ValidationError(Exception):85    def __init__(self(empty), errors{'age': 'Must be positive'}):86        self.errors→ {'age': 'Must be positive'} = errors{'age': 'Must be positive'}
  13. except ValidationError as e:

    101    raise ValidationError({"age": "Must be positive"})102except ValidationError as e:103    print(f"{eValidation failed: age - Must be positive}\n")104105try:
    outputValidation failed: age - Must be positive
    Validation failed: age - Must be positive
  14. self.errors ← {'username': 'Too short', 'email': 'Invalid format', 'age': 'Must be 18+'}

    pass 2 of 2
    84class ValidationError(Exception):85    def __init__(self(empty), errors{'username': 'Too short', 'email': 'Invalid format', 'age': 'Must be 18+'}):86        self.errors→ {'username': 'Too short', 'email': 'Invalid format', 'age': 'Must be 18+'} = errors{'username': 'Too short', 'email': 'Invalid format', 'age': 'Must be 18+'}
  15. except ValidationError as e:

    110    })111except ValidationError as e:112    print(f"{eMultiple validation errors:
      - username: Too short
      - email: Invalid format
      - age: Must be 18+}")113114# Exception with timestamp
    outputMultiple validation errors:
      - username: Too short
      - email: Invalid format
      - age: Must be 18+
    Multiple validation errors:
      - username: Too short
      - email: Invalid format
      - age: Must be 18+
  16. print(" Timestamped error:")

    126print("\nTimestamped error:")
    output
    Timestamped error:
  17. self.message ← Connection lost, self.timestamp ← 2025-01-15 10:30:00

    117class TimestampedError(Exception):118    def __init__(self(empty), messageConnection lost):119        self.message→ Connection lost = messageConnection lost120        self.timestamp→ 2025-01-15 10:30:00 = datetime(2025, 1, 15, 10, 30)
  18. except TimestampedError as e:

    129    raise TimestampedError("Connection lost")130except TimestampedError as e:131    print(f"  {e[2025-01-15 10:30:00] Connection lost}")132    print(f"  Time: {e.timestamp2025-01-15 10:30:00}")
    output  [2025-01-15 10:30:00] Connection lost
      Time: 2025-01-15 10:30:00
      Time: 2025-01-15 10:30:00
  19. main()

    134if __name__ == "__main__":135    main()

def __str__(self): controls what print(exception) shows.

When to create custom exceptions

Guidelines for when it's worth it.

when_to_use.py
Replay: real traced execution (multi-file project)
# When to create custom exceptions

def main():
    # Domain-specific errors
    class ShoppingCartError(Exception):
        """Base for shopping cart errors"""
        pass

    class ItemNotInCartError(ShoppingCartError):
        def __init__(self, item_id):
            self.item_id = item_id
            super().__init__(f"Item {item_id} not in cart")

    class CartLimitExceededError(ShoppingCartError):
        def __init__(self, limit, attempted):
            self.limit = limit
            self.attempted = attempted
            super().__init__(
                f"Cart limit {limit} exceeded: tried to add {attempted}"
            )

    class Cart:
        def __init__(self, max_items=5):
            self.items = []
            self.max_items = max_items

        def add(self, item):
            if len(self.items) >= self.max_items:
                raise CartLimitExceededError(
                    self.max_items,
                    len(self.items) + 1
                )
            self.items.append(item)

        def remove(self, item_id):
            for item in self.items:
                if item["id"] == item_id:
                    self.items.remove(item)
                    return
            raise ItemNotInCartError(item_id)

    print("Shopping cart with custom exceptions:\n")

    cart = Cart(max_items=3)

    # Add items
    try:
        cart.add({"id": 1, "name": "Laptop"})
        cart.add({"id": 2, "name": "Mouse"})
        cart.add({"id": 3, "name": "Keyboard"})
        print(f"  Added 3 items: {len(cart.items)} in cart")
    except CartLimitExceededError as e:
        print(f"  Error: {e}")

    # Try to exceed limit
    try:
        cart.add({"id": 4, "name": "Monitor"})
    except CartLimitExceededError as e:
        print(f"  Error: {e}")
        print(f"  Limit: {e.limit}, Attempted: {e.attempted}")

    # Remove item
    try:
        cart.remove(2)
        print(f"\n  Removed item 2: {len(cart.items)} in cart")
    except ItemNotInCartError as e:
        print(f"  Error: {e}")

    # Try to remove non-existent
    try:
        cart.remove(999)
    except ItemNotInCartError as e:
        print(f"  Error: {e}")
        print(f"  Item ID: {e.item_id}")


    # API-style exceptions
    class ApiException(Exception):
        def __init__(self, status_code, message, details=None):
            self.status_code = status_code
            self.message = message
            self.details = details
            super().__init__(message)

    class BadRequestError(ApiException):
        def __init__(self, message, details=None):
            super().__init__(400, message, details)

    class NotFoundError(ApiException):
        def __init__(self, resource, resource_id):
            super().__init__(
                404,
                f"{resource} not found",
                {"resource": resource, "id": resource_id}
            )

    class UnauthorizedError(ApiException):
        def __init__(self, message="Unauthorized"):
            super().__init__(401, message)

    def api_request(endpoint, user_id, auth_token):
        if not auth_token:
            raise UnauthorizedError("Missing auth token")

        if auth_token != "valid_token":
            raise UnauthorizedError("Invalid token")

        if not user_id:
            raise BadRequestError(
                "Missing required parameter",
                {"missing_field": "user_id"}
            )

        if user_id not in [1, 2, 3]:
            raise NotFoundError("User", user_id)

        return {"user_id": user_id, "data": "..."}

    print("\nAPI requests:")

    requests = [
        ("/users/1", 1, "valid_token"),
        ("/users/999", 999, "valid_token"),
        ("/users/1", None, "valid_token"),
        ("/users/1", 1, None),
        ("/users/1", 1, "bad_token")
    ]

    for endpoint, user_id, token in requests:
        try:
            result = api_request(endpoint, user_id, token)
            print(f"  {endpoint}: OK")
        except UnauthorizedError as e:
            print(f"  {endpoint}: {e.status_code} {e.message}")
        except NotFoundError as e:
            print(f"  {endpoint}: {e.status_code} {e.message}")
            print(f"    Details: {e.details}")
        except BadRequestError as e:
            print(f"  {endpoint}: {e.status_code} {e.message}")
            print(f"    Details: {e.details}")
        except ApiException as e:
            # Catch-all for other API errors
            print(f"  {endpoint}: {e.status_code} {e.message}")

if __name__ == "__main__":
    main()
  1. def main(): # Domain-specific errors

    3def main():4    # Domain-specific errors5    class ShoppingCartError(Exception):6        """Base for shopping cart errors"""7        pass8    9    class ItemNotInCartError(ShoppingCartError):10        def __init__(self, item_id):11            self.item_id = item_id12            super().__init__(f"Item {item_id} not in cart")13    14    class CartLimitExceededError(ShoppingCartError):15        def __init__(self, limit, attempted):16            self.limit = limit17            self.attempted = attempted18            super().__init__(19                f"Cart limit {limit} exceeded: tried to add {attempted}"20            )21    22    class Cart:23        def __init__(self, max_items=5):24            self.items = []25            self.max_items = max_items26        27        def add(self, item):28            if len(self.items) >= self.max_items:29                raise CartLimitExceededError(30                    self.max_items,31                    len(self.items) + 132                )33            self.items.append(item)34        35        def remove(self, item_id):36            for item in self.items:37                if item["id"] == item_id:38                    self.items.remove(item)39                    return40            raise ItemNotInCartError(item_id)41    42    print("Shopping cart with custom exceptions:\n")43    44    cart = Cart(max_items=3)
    outputShopping cart with custom exceptions:
  2. self.items ← [], self.max_items ← 3

    22class Cart:23    def __init__(self<__main__.main.<locals>.Cart object at ⟨addr A⟩>, max_items3=5):24        self.items→ [] = []25        self.max_items→ 3 = max_items3
  3. cart ← <__main__.main.<locals>.Cart object at ⟨addr A⟩>

    44cart→ <__main__.main.<locals>.Cart object at ⟨addr A⟩> = Cart(max_items=3)
  4. try:

    46# Add items47try:48    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 1, "name": "Laptop"})49    cart.add({"id": 2, "name": "Mouse"})
  5. self.items ← [{'id': 1, 'name': 'Laptop'}]

    pass 1 of 4
    27def add(self<__main__.main.<locals>.Cart object at ⟨addr A⟩>, item{'id': 1, 'name': 'Laptop'}):28    if len(self.items) >= self.max_items:29        raise CartLimitExceededError(30            self.max_items,31            len(self.items) + 132        )33    self.items→ [{'id': 1, 'name': 'Laptop'}].append(item{'id': 1, 'name': 'Laptop'})
    All 4 passes — pass 1 is the card above
    passitemself.max_itemslimitattemptedee.limite.attemptedcartitem_iditem[”id”]self.itemsself.limitself.attempted
    1{'id': 1, 'name': 'Laptop'}[] [{'id': 1, 'name': 'Laptop'}]
    2{'id': 2, 'name': 'Mouse'}[{'id': 1, 'name': 'Laptop'}] [{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}]
    3{'id': 3, 'name': 'Keyboard'}[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}] [{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}]
    4{'id': 4, 'name': 'Monitor'}334Cart limit 3 exceeded: tried to add 434<__main__.main.<locals>.Cart object at ⟨addr A⟩>22[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}] [{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}]34
  6. cart.add({"id": 1, "name": "Laptop"})

    47try:48    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 1, "name": "Laptop"})49    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 2, "name": "Mouse"})50    cart.add({"id": 3, "name": "Keyboard"})
  7. cart.add({"id": 2, "name": "Mouse"})

    48cart.add({"id": 1, "name": "Laptop"})49cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 2, "name": "Mouse"})50cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 3, "name": "Keyboard"})51print(f"  Added 3 items: {len(cart.items)} in cart")
  8. cart.add({"id": 3, "name": "Keyboard"})

    49    cart.add({"id": 2, "name": "Mouse"})50    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 3, "name": "Keyboard"})51    print(f"  Added 3 items: {len(cart.items[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}])} in cart")52except CartLimitExceededError as e:
    output  Added 3 items: 3 in cart
  9. try:

    55# Try to exceed limit56try:57    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.add({"id": 4, "name": "Monitor"})58except CartLimitExceededError as e:
  10. if len(self.items) >= self.max_items:

    27def add(self, item):28    if len(self.items[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}]) >= self.max_items3:29        raise CartLimitExceededError(30            self.max_items3,31            len(self.items[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}]) + 132        )33    self.items.append(item)
  11. self.limit ← 3, self.attempted ← 4

    14class CartLimitExceededError(ShoppingCartError):15    def __init__(self(3, 4), limit3, attempted4):16        self.limit→ 3 = limit317        self.attempted→ 4 = attempted418        super().__init__(
  12. except CartLimitExceededError as e:

    57    cart.add({"id": 4, "name": "Monitor"})58except CartLimitExceededError as e:59    print(f"  Error: {eCart limit 3 exceeded: tried to add 4}")60    print(f"  Limit: {e.limit3}, Attempted: {e.attempted4}")6162# Remove item
    output  Error: Cart limit 3 exceeded: tried to add 4
      Limit: 3, Attempted: 4
      Limit: 3, Attempted: 4
  13. try:

    62# Remove item63try:64    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.remove(2)65    print(f"\n  Removed item 2: {len(cart.items)} in cart")
  14. def remove(self, item_id):

    pass 1 of 2
    35def remove(self<__main__.main.<locals>.Cart object at ⟨addr A⟩>, item_id2):36    for item in self.items:37        if item["id"] == item_id:
  15. for item in self.items:

    pass 1 of 4
    35def remove(self, item_id):36    for item{'id': 1, 'name': 'Laptop'} in self.items[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}]:37        if item["id"] == item_id:38            self.items.remove(item)
    All 4 passes — pass 1 is the card above
    passitemitem[”id”]item_idself.items
    1{'id': 1, 'name': 'Laptop'}[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}]
    2{'id': 2, 'name': 'Mouse'}22[{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Mouse'}, {'id': 3, 'name': 'Keyboard'}] [{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}]
    3{'id': 1, 'name': 'Laptop'}[{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}]
    4{'id': 3, 'name': 'Keyboard'}[{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}]
  16. self.items ← [{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}]

    36for item in self.items:37    if item["id"]2 == item_id2:38        self.items→ [{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}].remove(item{'id': 2, 'name': 'Mouse'})39        return40raise ItemNotInCartError(item_id)
  17. cart.remove(2)

    63try:64    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.remove(2)65    print(f"\n  Removed item 2: {len(cart.items[{'id': 1, 'name': 'Laptop'}, {'id': 3, 'name': 'Keyboard'}])} in cart")66except ItemNotInCartError as e:
    output
      Removed item 2: 2 in cart
  18. try:

    69# Try to remove non-existent70try:71    cart<__main__.main.<locals>.Cart object at ⟨addr A⟩>.remove(999)72except ItemNotInCartError as e:
  19. def remove(self, item_id):

    pass 2 of 2
    35def remove(self<__main__.main.<locals>.Cart object at ⟨addr A⟩>, item_id999):36    for item in self.items:37        if item["id"] == item_id:
  20. raise ItemNotInCartError(item_id)

    39        return40raise ItemNotInCartError(item_id999)
  21. self.item_id ← 999

    9class ItemNotInCartError(ShoppingCartError):10    def __init__(self999, item_id999):11        self.item_id→ 999 = item_id99912        super().__init__(f"Item {item_id} not in cart")
  22. except ItemNotInCartError as e:

    71    cart.remove(999)72except ItemNotInCartError as e:73    print(f"  Error: {eItem 999 not in cart}")74    print(f"  Item ID: {e.item_id999}")7576#@help h1
    output  Error: Item 999 not in cart
      Item ID: 999
      Item ID: 999
  23. requests ← [('/users/1', 1, 'valid_token'), ('/users/999', 999, 'valid_token'), ('/users/1', None, 'valid_token'), ('/users/1', 1, None), ('/users/1', 1, 'bad_token')]

    76#@help h177# Create custom exceptions when:78# 1. Domain-specific errors need special handling79# 2. Need to attach context data80# 3. Want to catch specific errors differently81# 4. Building a library/API for others82#@end8384# API-style exceptions85class ApiException(Exception):86    def __init__(self, status_code, message, details=None):87        self.status_code = status_code88        self.message = message89        self.details = details90        super().__init__(message)9192class BadRequestError(ApiException):93    def __init__(self, message, details=None):94        super().__init__(400, message, details)9596class NotFoundError(ApiException):97    def __init__(self, resource, resource_id):98        super().__init__(99            404,100            f"{resource} not found",101            {"resource": resource, "id": resource_id}102        )103104class UnauthorizedError(ApiException):105    def __init__(self, message="Unauthorized"):106        super().__init__(401, message)107108def api_request(endpoint, user_id, auth_token):109    if not auth_token:110        raise UnauthorizedError("Missing auth token")111    112    if auth_token != "valid_token":113        raise UnauthorizedError("Invalid token")114    115    if not user_id:116        raise BadRequestError(117            "Missing required parameter",118            {"missing_field": "user_id"}119        )120    121    if user_id not in [1, 2, 3]:122        raise NotFoundError("User", user_id)123    124    return {"user_id": user_id, "data": "..."}125126print("\nAPI requests:")127128requests→ [('/users/1', 1, 'valid_token'), ('/users/999', 999, 'valid_token'), ('/users/1', None, 'valid_token'), ('/users/1', 1, None), ('/users/1', 1, 'bad_token')] = [129    ("/users/1", 1, "valid_token"),130    ("/users/999", 999, "valid_token"),131    ("/users/1", None, "valid_token"),132    ("/users/1", 1, None),133    ("/users/1", 1, "bad_token")134]
    output
    API requests:
  24. for endpoint, user_id, token in requests:

    pass 1 of 5
    136for endpoint/users/1, user_id1, tokenvalid_token in requests[('/users/1', 1, 'valid_token'), ('/users/999', 999, 'valid_token'), ('/users/1', None, 'valid_token'), ('/users/1', 1, None), ('/users/1', 1, 'bad_token')]:137    try:138        result = api_request(endpoint, user_id, token)
    All 5 passes — pass 1 is the card above
    passendpointuser_idtokenselfresourceresource_ide.status_codee.messagee.detailsmessagedetailsNoneauth_token
    1/users/11valid_token
    2/users/999999valid_token('User', 999)User999404User not found{'resource': 'User', 'id': 999}
    3/users/1Nonevalid_token('Missing required parameter', {'missing_field': 'user_id'})400Missing required parameter{'missing_field': 'user_id'}Missing required parameter{'missing_field': 'user_id'}None
    4/users/11NoneMissing auth token401Missing auth tokenMissing auth tokenNone
    5/users/11bad_tokenInvalid token401Invalid tokenInvalid tokenbad_token
  25. try:

    pass 1 of 5
    136for endpoint, user_id, token in requests:137    try:138        result = api_request(endpoint/users/1, user_id1, tokenvalid_token)139        print(f"  {endpoint}: OK")
    All 5 passes — pass 1 is the card above
    passendpointuser_idtokenselfresourceresource_ide.status_codee.messagee.detailsmessagedetailsNoneauth_token
    1/users/11valid_token
    2/users/999999valid_token('User', 999)User999404User not found{'resource': 'User', 'id': 999}
    3/users/1Nonevalid_token('Missing required parameter', {'missing_field': 'user_id'})400Missing required parameter{'missing_field': 'user_id'}Missing required parameter{'missing_field': 'user_id'}None
    4/users/11NoneMissing auth token401Missing auth tokenMissing auth tokenNone
    5/users/11bad_tokenInvalid token401Invalid tokenInvalid tokenbad_token
  26. def api_request(endpoint, user_id, auth_token):

    pass 1 of 5
    108def api_request(endpoint/users/1, user_id1, auth_tokenvalid_token):109    if not auth_token:110        raise UnauthorizedError("Missing auth token")111    112    if auth_token != "valid_token":113        raise UnauthorizedError("Invalid token")114    115    if not user_id:116        raise BadRequestError(117            "Missing required parameter",118            {"missing_field": "user_id"}119        )120    121    if user_id not in [1, 2, 3]:122        raise NotFoundError("User", user_id)123    124    return {"user_id": user_id1, "data": "..."}
    All 5 passes — pass 1 is the card above
    passendpointuser_idauth_tokenselfresourceresource_ide.status_codee.messagee.detailsmessagedetailsNone
    1/users/11valid_token
    2/users/999999valid_token('User', 999)User999404User not found{'resource': 'User', 'id': 999}
    3/users/1Nonevalid_token('Missing required parameter', {'missing_field': 'user_id'})400Missing required parameter{'missing_field': 'user_id'}Missing required parameter{'missing_field': 'user_id'}None
    4/users/11NoneMissing auth token401Missing auth tokenMissing auth token
    5/users/11bad_tokenInvalid token401Invalid tokenInvalid token
  27. result ← {'user_id': 1, 'data': '...'}

    137try:138    result→ {'user_id': 1, 'data': '...'} = api_request(endpoint/users/1, user_id1, tokenvalid_token)139    print(f"  {endpoint/users/1}: OK")140except UnauthorizedError as e:
    output  /users/1: OK
  28. if user_id not in [1, 2, 3]:

    121if user_id999 not in [1, 2, 3]:122    raise NotFoundError("User", user_id999)
  29. def __init__(self, resource, resource_id):

    96class NotFoundError(ApiException):97    def __init__(self('User', 999), resourceUser, resource_id999):98        super().__init__(99            404,
  30. self.status_code ← 404, self.message ← User not found, self.details ← {'resource': 'User', 'id': 999}

    pass 1 of 4
    85class ApiException(Exception):86    def __init__(self('User', 999), status_code404, messageUser not found, details{'resource': 'User', 'id': 999}=NoneNone):87        self.status_code→ 404 = status_code40488        self.message→ User not found = messageUser not found89        self.details→ {'resource': 'User', 'id': 999} = details{'resource': 'User', 'id': 999}90        super().__init__(message)
    All 4 passes — pass 1 is the card above
    passselfstatus_codemessagedetailsendpointe.status_codee.messagee.detailsuser_idauth_tokenself.status_codeself.messageself.details
    1('User', 999)404User not found{'resource': 'User', 'id': 999}/users/999404User not found{'resource': 'User', 'id': 999}None404User not found{'resource': 'User', 'id': 999}
    2('Missing required parameter', {'missing_field': 'user_id'})400Missing required parameter{'missing_field': 'user_id'}/users/1400Missing required parameter{'missing_field': 'user_id'}None400Missing required parameter{'missing_field': 'user_id'}
    3Missing auth token401Missing auth tokenNone/users/1401Missing auth tokenbad_token401Missing auth tokenNone
    4Invalid token401Invalid tokenNone/users/1401Invalid token401Invalid tokenNone
  31. except NotFoundError as e:

    141    print(f"  {endpoint}: {e.status_code} {e.message}")142except NotFoundError as e:143    print(f"  {endpoint/users/999}: {e.status_code404} {e.messageUser not found}")144    print(f"    Details: {e.details{'resource': 'User', 'id': 999}}")145except BadRequestError as e:
    output  /users/999: 404 User not found
        Details: {'resource': 'User', 'id': 999}
  32. if not user_id:

    115if not user_idNone:116    raise BadRequestError(117        "Missing required parameter",118        {"missing_field": "user_id"}119    )
  33. def __init__(self, message, details=None):

    92class BadRequestError(ApiException):93    def __init__(self('Missing required parameter', {'missing_field': 'user_id'}), messageMissing required parameter, details{'missing_field': 'user_id'}=NoneNone):94        super().__init__(400, message, details)
  34. except BadRequestError as e:

    144    print(f"    Details: {e.details}")145except BadRequestError as e:146    print(f"  {endpoint/users/1}: {e.status_code400} {e.messageMissing required parameter}")147    print(f"    Details: {e.details{'missing_field': 'user_id'}}")148except ApiException as e:
    output  /users/1: 400 Missing required parameter
        Details: {'missing_field': 'user_id'}
  35. if not auth_token:

    108def api_request(endpoint, user_id, auth_token):109    if not auth_tokenNone:110        raise UnauthorizedError("Missing auth token")
  36. def __init__(self, message="Unauthorized"):

    pass 1 of 2
    104class UnauthorizedError(ApiException):105    def __init__(selfMissing auth token, messageMissing auth token="Unauthorized"):106        super().__init__(401, message)
  37. except UnauthorizedError as e:

    pass 1 of 2
    139    print(f"  {endpoint}: OK")140except UnauthorizedError as e:141    print(f"  {endpoint/users/1}: {e.status_code401} {e.messageMissing auth token}")142except NotFoundError as e:
    output  /users/1: 401 Missing auth token
  38. if auth_token != "valid_token":

    112if auth_tokenbad_token != "valid_token":113    raise UnauthorizedError("Invalid token")
  39. def __init__(self, message="Unauthorized"):

    pass 2 of 2
    104class UnauthorizedError(ApiException):105    def __init__(selfInvalid token, messageInvalid token="Unauthorized"):106        super().__init__(401, message)
  40. except UnauthorizedError as e:

    pass 2 of 2
    139    print(f"  {endpoint}: OK")140except UnauthorizedError as e:141    print(f"  {endpoint/users/1}: {e.status_code401} {e.messageInvalid token}")142except NotFoundError as e:
    output  /users/1: 401 Invalid token
  41. main()

    152if __name__ == "__main__":153    main()

Create custom exceptions when you need specific handling or extra data.

Exercise: practical.py

Build a web API with domain-specific exceptions