Legends ofPythos
Claim your name
Writing Real Programs

Your own exceptions

Lesson 10 of 16

Watch the lesson1:00 · with Sigrid
Built-in errors like ValueError and TypeError are useful, but they often hide the real problem. When your program fails because a user has no credit left, or an item is out of stock, you want a name that means something to you. That is where custom exceptions come in.

Defining the minimum

class InsufficientFunds(Exception):
    """Raised when a transaction exceeds available balance."""

try:
    raise InsufficientFunds("Not enough money")
except InsufficientFunds as e:
    print(e)
A custom exception class and how to use it

Why specific names beat generic catches

Catching Exception for everything is like putting a band-aid on every cut. If your function can fail because the port is wrong, or because the file does not exist, you want two different names so callers know exactly what went wrong and how to react.

Exception families

class ShopError(Exception):
    """Base for all shop-related problems."""

class OutOfStock(ShopError):
    pass

def buy(item, qty=1):
    if item == 'coffee':
        raise OutOfStock("Coffee is gone")
    return f'Purchased {qty} x {item}'

try:
    buy('coffee')
except ShopError as e:
    print(f"Shop problem: {e}")
Catch the whole family with except ShopError
Because OutOfStock inherits from ShopError, a single except ShopError catches both. This keeps your error handling tidy when you have many related failure modes.

Chaining and notes

class ConfigError(Exception):
    pass

def read_level(settings):
    try:
        return int(settings['level'])
    except ValueError as err:
        raise ConfigError('level must be a number') from err
    except KeyError as err:
        err.add_note('check the settings for a level entry')
        raise
Chaining with from and adding context notes
raise ... from err attaches the original error as __cause__, so tracebacks show both. .add_note() (Python 3.11+) lets you add extra context without changing the exception type.

Your turn

0 of 3 solved

Exercise 1

+35 XP
Define an exception class InsufficientFunds. Write withdraw(balance, amount), which returns the new balance, or raises InsufficientFunds with the message f'need {short} more' when amount is bigger than balance, where short is the difference. Then write safe_withdraw(balance, amount), which calls withdraw and, if it raises InsufficientFunds, catches it by name and returns its message as a string instead. For example, safe_withdraw(10, 25) is 'need 15 more'.
class InsufficientFunds(Exception):
    pass




def withdraw(balance, amount):
    pass




def safe_withdraw(balance, amount):
    # try withdraw; on InsufficientFunds return the message
    pass

Run your code to check it against the tests.

Exercise 2

+35 XP
Build a small family of exceptions: ShopError inherits from Exception, and both OutOfStock and UnknownItem inherit from ShopError. Write buy(stock, item): it raises UnknownItem(f'no such item: {item}') if the item is not in the stock dictionary, OutOfStock(f'no {item} left') if its count is 0, and otherwise takes one away and returns how many are left. Then write try_buy(stock, item), which returns what buy returns, or catches the whole family with except ShopError and returns 'sorry: ' plus the message.
class ShopError(Exception):
    pass




# OutOfStock and UnknownItem go here




def buy(stock, item):
    pass




def try_buy(stock, item):
    pass

Run your code to check it against the tests.

Exercise 3

+35 XP
Define ConfigError(Exception) and write read_port(settings), which returns int(settings['port']). Turn the two ways that can fail into a ConfigError chained from the original with raise ... from error: a missing key (KeyError) raises ConfigError('no port'), and a value that is not a number (ValueError) raises ConfigError(f'bad port: {value}'). The caught helper at the top is for the tests.
def caught(action):
    """Run action and hand back the exception it raised, or None. The tests use this."""
    try:
        action()
    except Exception as error:
        return error
    return None




class ConfigError(Exception):
    pass




def read_port(settings):
    # int(settings['port']), turning KeyError and ValueError into ConfigError with raise ... from
    pass

Run your code to check it against the tests.