Properties: computed and validated attributes
A property looks like an attribute to the code that uses it, but runs a method when it is read, and optionally another when it is set. That lets a class compute values on demand and refuse bad assignments.
- Turn a computed method into a read-only attribute with
@property - Add a setter with
@name.setterthat converts and validates assigned values - Run the setter's checks in
__init__too, so construction and later changes obey the same rule
In the Classes & Objects module a line's total was either stored (and went stale when the quantity changed) or a method (line.total()). A property gives you both: callers write line.total like an attribute, and every read runs a method, so it is always current.
Put @property above a method that takes only self. The method's name becomes the attribute name. There is no setter yet, so assigning to it is an error: a read-only attribute.
Run each example to see what it prints. Running examples doesn't count toward progress.
class OrderLine: def __init__(self, sku, quantity, unit_price): self.sku = sku self.quantity = quantity self.unit_price = unit_price @property def total(self): return round(self.quantity * self.unit_price, 2) line = OrderLine("SPO-11681", 2, 17.92) print(line.total) line.quantity = 3 print(line.total)
class OrderLine: def __init__(self, sku, quantity, unit_price): self.sku = sku self.quantity = quantity self.unit_price = unit_price @property def total(self): return round(self.quantity * self.unit_price, 2) line = OrderLine("SPO-11681", 2, 17.92) print(line.total) line.total = 30.0
A property can also have a setter: @price.setter above a second method with the same name, taking the new value. It runs on every assignment, so it can convert and validate. The value itself is stored under a different name, by convention with a leading underscore (self._price), which means "internal: use the property".
If __init__ assigns through the property (self.price = price), the setter's checks also run when the object is created, so there is one rule for both.
class Product: def __init__(self, sku, price): self.sku = sku self.price = price @property def price(self): return self._price @price.setter def price(self, value): value = round(float(value), 2) if value < 0: raise ValueError(f"{self.sku}: price can't be negative ({value})") self._price = value mat = Product("SPO-11681", "31") print(mat.price) mat.price = 27.499 print(mat.price) for bad in [-5, "free"]: try: mat.price = bad except ValueError as err: print("refused:", err) print(mat.price) try: Product("HOM-22847", -48) except ValueError as err: print("refused:", err)
When to use it: for values derived from other attributes (totals, full names, flags), and for attributes with rules that must hold after every change. Start with plain attributes; turning one into a property later doesn't change any calling code.
Mistakes to avoid: storing the value under the property's own name inside the setter (self.price = value calls the setter again, forever: RecursionError); slow work in a property (callers expect attribute reads to be cheap); and properties with surprising side effects.
@propertyruns a method when an attribute is read, so derived values are always current.@name.setterruns on assignment; store the value under an internal name such as_price.- Assign through the property in
__init__so creation and later changes follow the same rule. - Next: special methods that make objects print, compare, add up and answer
lenandin.