Definition
Picture a Rectangle class, with a width and a height stored at creation time. You want the calling code to read an area without redoing the multiplication every time. Two solutions exist, and neither is really enough. The first stores the area in a plain attribute, computed at creation: quick to read, but stale the moment the width changes afterwards. The second exposes an area() method, always correct, but it forces every caller to write parentheses, revealing a detail they have no reason to know.
property removes that impossible choice. The decorator @property, placed right above a method, makes it look like an attribute: the result is read without parentheses, like a value stored on the object, while a computation quietly runs behind it on every read.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
r = Rectangle(3, 4)
print(r.area) # 12, not r.area()To the calling code, r.area looks exactly like r.width: nothing tells the reader a multiplication just happened, because nothing requires them to know. The computed attribute also stays in step with the values it depends on, something no plain attribute can promise.
Under the hood, @property relies on the descriptor protocol, the mechanism that also makes ordinary methods work. That is why a property has to live on the class: a descriptor only fires from that exact spot, never from a single object.
Why not a plain attribute
Many languages push developers to wrap every value in a getter and a setter, as a precaution, from day one. Python does the opposite: the plain attribute is exposed first, and the day validation becomes necessary, a property slides into its place. The public face does not move: product.price still reads and writes exactly as before, only what happens behind those lines has changed.
class Product:
def __init__(self, price):
self.price = price # already goes through the setter
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("A price cannot be negative")
self._price = valueCode written yesterday still writes product.price = 12, except a negative price now makes Python raise a ValueError instead of travelling quietly through the rest of the application. The real value sits in _price: the leading underscore signals by convention that this attribute belongs to the inner workings of the class, and that nothing outside should reach for it directly.
Read, write, delete
A property covers three distinct accesses, each wired to its own decorator, summed up below. Only the first one is required, the other two are added when the need actually shows up.
| Decorator | Access covered | Triggered by |
|---|---|---|
@property | Read | product.price |
@price.setter | Write | product.price = 12 |
@price.deleter | Delete | del product.price |
With no setter, the property stays read only: any attempted assignment raises a clear AttributeError, instead of silently overwriting a value that was never meant to move. That is a tidy way of making a value untouchable from the outside, more reliable than a comment politely asking people not to touch it.
The classic traps
Three traps come up again and again for anyone discovering property, and knowing them ahead of time saves an hour lost staring at a puzzling message.
The first is infinite recursion, easy to write without noticing. A setter writing self.price instead of self._price calls itself forever, since that assignment triggers the setter again instead of touching the internal attribute.
@price.setter
def price(self, value):
self.price = value # calls itself again, RecursionErrorThe program then stops on a RecursionError whose message never mentions the property at all: you have to walk back up the call stack to see that the setter is calling itself in a loop.
The second trap concerns where it is declared. Attached after the fact to a single instance rather than to the class, it is stored as an ordinary object and never fires: reading that attribute hands back the property itself, instead of the expected result.
The third trap costs the most, because it does not show up right away. A read that looks like an attribute invites repetition without a second thought, loops included.
If the property queries a database or reads a file again, the same request goes out on every read, a hundred times for a hundred loop rounds. It should stay cheap, or state its price plainly by turning back into an ordinary method, parentheses included.
Frequently asked questions
Should every attribute get a property?
No, and that is the most common mistake among people who just discovered the tool. A property that merely hands back self._value without checking anything adds code without adding anything. A plain attribute, or a dataclass, does the job as long as no computation and no validation are involved.
How can an expensive property avoid being recomputed?
With functools.cached_property, which computes on first access and then stores the result on the object, never touching it again. The flip side deserves attention: the value no longer refreshes when the data it depends on changes, which reserves it for objects that stop moving once built.
What sets it apart from classmethod and staticmethod?
Those two remain methods called with parentheses: classmethod receives the class instead of the object, staticmethod receives nothing at all. A property does not change what the method receives, only the way it is reached.