property in Python: a computed attribute that reads like any other

A property makes a method look like an attribute: read it without parentheses, validate on write, change nothing for the caller.
5 min read
Believemy logo

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.

PYTHON
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.

Good to know

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.

PYTHON
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 = value

Code 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.

DecoratorAccess coveredTriggered by
@propertyReadproduct.price
@price.setterWriteproduct.price = 12
@price.deleterDeletedel 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.

PYTHON
@price.setter
def price(self, value):
    self.price = value   # calls itself again, RecursionError

The 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.

Warning

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

Question

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.

Question

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.

Question

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.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.