An invariant is a statement that must hold whenever callers use an object’s public interface. For a stock item, the quantity is an actual integer and is never negative. The SKU is meaningful text. These rules belong to the object’s whole lifetime, so checking them in just one client function is insufficient. The constructor and every operation that can change stock must preserve them.
__init__ initializes an instance that Python has already created. It should
return None; it does not return the new object. If initialization raises an
exception, the class call fails and the caller does not receive a normally
constructed value. Validate all required inputs before publishing useful state.
This course does not customize __new__ or expose partly initialized objects.
Keep the public write path small
A getter-only property exposes data while directing normal updates through a
checked method. The decorator replaces a method definition with a property:
callers write item.quantity, while the getter computes or returns the value.
A property without a setter rejects ordinary assignment with AttributeError.
It is an interface rule, not a defense against arbitrary hostile Python code.
class Stock:
def __init__(self, quantity):
if type(quantity) is not int:
raise TypeError("quantity must be an integer")
if quantity < 0:
raise ValueError("negative quantity")
self._quantity = quantity
@property
def quantity(self):
return self._quantity
def adjust(self, delta):
if type(delta) is not int:
raise TypeError("delta must be an integer")
candidate = self._quantity + delta
if candidate < 0:
raise ValueError("insufficient stock")
self._quantity = candidate
item = Stock(3)
try:
item.adjust(-4)
except ValueError:
print("rejected", item.quantity)
item.adjust(-3)
print(item.quantity)The order of those statements provides an important guarantee: a rejected adjustment leaves the old quantity unchanged. Compute a candidate in a local variable, check it, then assign it. Writing the backing attribute first and checking afterward would leave invalid state behind when an exception escapes.
Precise types and boundaries
Python treats bool as a subclass of int, so isinstance(True, int) succeeds.
This stock contract needs exact integers, making type(value) is int a deliberate
choice. Converting with int() would accept text or truncate a float and change
the contract. The lab distinguishes a wrong type (TypeError) from an integer
outside the allowed range (ValueError).
Zero is a valid quantity and a valid adjustment. An item can be depleted exactly; a test only for positive examples misses that boundary. A blank SKU includes spaces and tabs, but a nonblank SKU retains its original spelling. Validation and normalization are different API decisions; do only the one the contract asks for.
Trace construction with zero, construction with minus one, exact depletion, and one unit too much. After each failure, inspect the same object again. The bug hunts target a constructor loophole and a check against old state. The fill practices validate-before-assign; the build adds strict SKU handling and read-only properties without introducing network or storage concerns.