Validating Properties
Source: Fluent Python, 2nd Edition — Chapter 23 by Luciano Ramalho (LineItem through attribute APIs / chapter summary)
23a used properties to compute (and cache) linked records. This half: properties as validation without changing item.weight = 12, a factory so weight and price are not two copy-pasted setters, then del, dir/getattr, and __getattribute__ vs __getattr__. Descriptors proper are Chapter 24.
LineItem #1 — garbage in
Organic bulk: description, weight, price, subtotal(). raisins.weight = -20 → negative money. Bezos: early Amazon let people order a negative quantity of books and credited the card. Java-style getters would work; if production already uses item.weight, replace the data attribute with a property and keep the spelling.
LineItem #2 — @weight.setter
@property
def weight(self):
return self.__weight
@weight.setter
def weight(self, value):
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
__init__ assigns self.weight = weight — the setter already runs, so LineItem('walnuts', 0, 10) dies. Storage is name-mangled __weight. The getter, setter, and public name are all weight. Same trick for price is the repetition Graham hates. Factory now; descriptor class in 24 (property is a descriptor).
property is a class: property(fget=None, fset=None, fdel=None, doc=None). Missing piece → that operation is forbidden. Decorators arrived in 2.4; before that:
weight = property(get_weight, set_weight)
Factories still like this form. Decorators win in a busy class body (no get_/set_ convention required).
Starter #1 accepts 0. Second test is the Bezos case.
Properties vs instance attributes
Properties live on the class but intercept instances. Search for obj.data starts at obj.__class__. If that class (or a base) has an overriding descriptor named data (a property is one), the instance dict is not used for that name.
Demo:
Class.data = 'the class data attr'plus@property def prop. Emptyvars(obj).obj.datareads the class string.obj.data = 'bar'creates an instance attr;Class.dataunchanged. Data attributes can be shadowed.obj.prop = 'foo'→AttributeError: can't set attribute(no setter).obj.__dict__['prop'] = 'foo'succeeds;obj.propstill runs the getter.- Replace
Class.propwith a plain string: nowobj.propis'foo'. - Replace
Class.datawith aproperty:obj.datasuddenly uses the property, even thoughvars(obj)still has'bar'.del Class.datarestores the instance value.
Docs
help() reads property.__doc__. Classic: property(..., doc='weight in kilograms'). Decorator: getter docstring is the property’s docs.
quantity factory
class LineItem:
weight = quantity('weight') # name twice: RHS runs before the name exists
price = quantity('price')
Fixing the doubled name is a class decorator / metaclass job (25). Closure:
def quantity(storage_name):
def qty_getter(instance):
return instance.__dict__[storage_name]
def qty_setter(instance, value):
if value > 0:
instance.__dict__[storage_name] = value
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter)
First arg is instance, not self — this is not a class body. Must hit __dict__ or the property recurses. vars(nutmeg) shows weight and price sitting next to description; reads still go through the property.
Same factory in a util module; later a descriptor hierarchy.
Where lookup starts
1 / 2obj.prop when Class.prop is a property and obj.__dict__ has prop
Pick an answer to see if you are right.
Empty property() cannot set weight; neg and ok both miss until the factory is real. Two fields share one function — do not hardcode only 'weight'.
Deletion
del obj.an_attribute. Rare. @member.deleter or property(..., fdel=...). Black Knight: phrases queue; del knight.member pops and prints. Without a property: __delattr__.
Cheat sheet
__class__ | type(obj). Special methods are found on the class, not a same-named instance attr. |
__dict__ | Writable attrs. Missing if __slots__ without '__dict__'. |
__slots__ | Allowed names; optional '__dict__' in the tuple. |
dir | Interactive “interesting” names; not __dict__ itself but its keys; skips some class guts (__mro__). No arg → current scope. Handles slotted instances. |
getattr / hasattr / setattr | hasattr is getattr + catch AttributeError. |
vars(obj) | __dict__ only; fails on slotted-without-dict. vars() = locals(). |
Dot or these builtins fire the matching special method. obj.__dict__['x'] = does not.
__delattr__— everydel obj.attr__dir__—dir(obj)__getattr__— after instance, class, bases fail__getattribute__— every fetch except some dunders;__getattr__only if this raisesAttributeError. Recursion:super().__getattribute__(name)__setattr__— every store
Unconditional __getattribute__ / __setattr__ are easier to get wrong than __getattr__ or a property.