runtime
128 / 250
lesson 128 Classes & Objects · advanced

Constructors that keep objects valid

If __init__ converts and checks its arguments and raises ValueError for bad ones, an invalid object can never exist, and the rest of the program can trust every object it gets.

You'll be able to
  • Convert and tidy arguments in __init__ (strip, upper, float) before storing them
  • Raise ValueError from __init__ so an invalid object is never created
  • Give parameters defaults, and avoid the shared mutable default trap
The idea

Data arrives messy: SKUs in lower case with spaces, prices as text, negative stock from a typo. If every function that uses a product has to check all of that, the checks get forgotten somewhere. Put them in one place instead: the constructor. __init__ can tidy each argument, convert it, and raise ValueError(...) (see Exceptions & Validation) when it can't be made valid. Because the error stops the object from being created, every Product that *does* exist is valid, and the rest of the program can rely on it.

A parameter can have a default (stock=0, the Functions module), so callers can leave it out.

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

example-1.py · A product that checks itself
import re

class Product:
    def __init__(self, sku, name, price, stock=0):
        sku = sku.strip().upper()
        if not re.fullmatch(r"[A-Z]{3}-\d{5}", sku):
            raise ValueError(f"bad SKU: {sku!r}")
        price = float(price)
        if price < 0:
            raise ValueError(f"negative price for {sku}: {price}")
        self.sku = sku
        self.name = name.strip()
        self.price = price
        self.stock = int(stock)

mat = Product(" spo-11681", "Yoga mat ", "31.00", "12")
print(mat.sku, repr(mat.name), mat.price, mat.stock)
lamp = Product("HOM-22847", "Desk lamp", 48)
print(lamp.sku, lamp.stock)
for args in [("SPO-1168", "Mat", 5), ("ELE-24781", "Speaker", -1)]:
    try:
        Product(*args)
    except ValueError as err:
        print("refused:", err)

Defaults need one warning. A default value is created once, when def runs, not each time the function is called. For a number or a text that doesn't matter. For a list, every object that uses the default shares that one list.

example-2.py · Two baskets, one default list
class Basket:
    def __init__(self, customer_id, items=[]):
        self.customer_id = customer_id
        self.items = items

nora = Basket("C00038")
nora.items.append("SPO-11681")
jamal = Basket("C00077")
assert jamal.items == [], f"Jamal's new basket already holds {jamal.items}"

The fix is a default of None, replaced inside __init__ by a new list. While at it, make a copy of a list the caller passes in, so the object doesn't share the caller's list either.

example-3.py · The None default
class Basket:
    def __init__(self, customer_id, items=None):
        if not customer_id:
            raise ValueError("a basket needs a customer")
        self.customer_id = customer_id
        if items is None:
            items = []
        self.items = list(items)

nora = Basket("C00038")
nora.items.append("SPO-11681")
jamal = Basket("C00077")
saved = ["HOM-22847", "ELE-24781"]
grace = Basket("C01338", saved)
grace.items.append("TOY-54564")
print(nora.items, jamal.items)
print(grace.items, saved)

When to use it: for any class whose objects must obey rules (prices not negative, ids in a format, quantities whole and positive). Validate where the object is created, not in every function that uses it.

Mistakes to avoid: storing first and checking later (an exception halfway leaves no object, but checks after self.x = … read badly); assert instead of raise (asserts can be switched off, the Testing & Code Quality module); mutable defaults such as items=[]; and error messages that don't say which value was wrong.

Review
  • __init__ can tidy, convert and check its arguments before storing them.
  • Raising ValueError there means an invalid object is never created.
  • Defaults are made once: use None and create a new list inside, and copy lists the caller passes in.
  • Next: methods, the functions that belong to a class.
Practise this

Loading your progress…

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

Constructors that keep objects valid — Classes & Objects · Runtime