Contracts, invariants, and exceptions

Design by contract describes a function by three things: preconditions that callers must satisfy, postconditions that the function guarantees, and class invariants that stay true whenever no method is running. When a precondition is violated, the caller has a bug. When a postcondition or invariant fails, the function itself has a bug.

Python
class InsufficientFunds(Exception):
    pass


class Account:
    def __init__(self, balance):
        if balance < 0:
            raise ValueError("opening balance cannot be negative")
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("amount must be positive")
        if amount > self._balance:
            raise InsufficientFunds(f"balance {self._balance} is less than {amount}")
        self._balance -= amount
        assert self._balance >= 0
        return self._balance


account = Account(100)
print(account.withdraw(30))
for amount in (0, 500):
    try:
        account.withdraw(amount)
    except InsufficientFunds as error:
        print("rejected:", error)
    except ValueError as error:
        print("invalid:", error)
print(account.balance)
Read the full lesson