runtime
90 / 250
lesson 90 Exceptions & Validation · intermediate

Your own kinds of error

`class OutOfStock(Exception): pass` makes a new error type. Callers can catch exactly that problem, and nothing else.

You'll be able to
  • Define an error type with class Name(Exception): pass and raise it with a message
  • Catch your own error by name while other errors still escape
  • Use different error types to choose different responses in a caller
The idea

ValueError is shared by every library you use. If your stock function raises ValueError when an item is sold out, a caller can't tell "sold out" from "someone typed twelve". A custom error type gives your problem its own name.

The pattern is two lines: class OutOfStock(Exception): and an indented pass. class makes a new type; (Exception) says it is a kind of error, so it can be raised and caught; pass means it needs nothing else. Classes can do much more, and the Classes & Objects module teaches them properly. For errors, this pattern is all you need.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Sold out has its own name
class OutOfStock(Exception):
    pass

stock = {"P0164": 3, "P0594": 0}

def reserve(sku, qty):
    if stock[sku] < qty:
        raise OutOfStock(f"{sku}: wanted {qty}, only {stock[sku]} left")
    stock[sku] -= qty

for sku, qty in [("P0164", 2), ("P0594", 1), ("P0164", 5)]:
    try:
        reserve(sku, qty)
        print("reserved", qty, sku)
    except OutOfStock as err:
        print("backorder:", err)
print(stock)
example-2.py · Other errors still escape
class OutOfStock(Exception):
    pass

stock = {"P0164": 3}

def reserve(sku, qty):
    if stock[sku] < qty:
        raise OutOfStock(f"{sku}: only {stock[sku]} left")
    stock[sku] -= qty

try:
    reserve("P9999", 1)
except OutOfStock as err:
    print("backorder:", err)

Different error types let one caller respond differently. A feed importer can skip a row that is bad, but must stop if the file itself is the wrong one. Two error types make that decision readable.

example-3.py · Skip a row, or stop the import
class BadRow(Exception):
    pass

class WrongFeed(Exception):
    pass

def read_row(row):
    if "sku" not in row:
        raise WrongFeed("no sku column: is this the price feed?")
    if row["price"] < 0:
        raise BadRow(f"{row['sku']}: negative price {row['price']}")
    return (row["sku"], row["price"])

def import_feed(rows):
    prices = {}
    skipped = []
    for row in rows:
        try:
            sku, price = read_row(row)
        except BadRow as err:
            skipped.append(str(err))
        else:
            prices[sku] = price
    return prices, skipped

print(import_feed([{"sku": "SPO-11681", "price": 7.49}, {"sku": "HOM-22847", "price": -31.0}]))
try:
    import_feed([{"product": "x", "price": 1.0}])
except WrongFeed as err:
    print("import stopped:", err)

When to use it: when callers need to tell your failure apart from Python's own, or from each other. Mistakes to avoid: forgetting (Exception) (then it can't be raised: TypeError: exceptions must derive from BaseException); raising the class with no message; and defining many error types nobody catches separately: one per decision a caller makes is enough.

Review
  • class Name(Exception): pass makes an error type; raise it with a message like any other.
  • except Name: catches exactly your problem; everything else still escapes.
  • Separate types let callers choose: skip a bad row, stop on a wrong file.
  • Next module: files. Opening, reading and writing them is where these errors meet real data.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Your own kinds of error — Exceptions & Validation · Runtime