datarekha
Python Easy Asked at GoogleAsked at AmazonAsked at MicrosoftAsked at Airbnb

How does the `@property` decorator work in Python, and when should you prefer it over a plain attribute?

The short answer

`@property` turns a method into a descriptor that Python calls automatically on attribute access, letting you add validation or computation behind a dot-access interface without changing callers. Use it when a value is derived, needs guarding, or must be lazily computed — not as a default for every attribute.

How to think about it

When an interviewer asks about @property, they are rarely interested in the syntax. They want to know whether you grasp a deeper Python idea — that attribute access can be intercepted — and whether you have the taste to use that power sparingly.

Here is the problem it solves. You ship a class with a plain radius attribute, and callers happily write c.radius = 5. Months later you need to reject negative radii. Without @property, your only escape is to rename the attribute to get_radius() and set_radius() — and now every line that ever touched .radius, in your code and everyone else’s, has to change. @property removes that wall: the dot-access interface stays exactly as it was, and the validation slips in quietly behind it.

A worked example

A property turns a method into something Python calls for you on access. Watch what c.radius = -3 does at the very end:

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius          # already runs the setter below

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError(f"radius must be >= 0, got {value}")
        self._radius = value

    @property
    def area(self):
        return math.pi * self._radius ** 2

    @property
    def diameter(self):
        return self._radius * 2

c = Circle(5)
print("radius  :", c.radius)
print("diameter:", c.diameter)
print("area    :", round(c.area, 4))

c.radius = 10
print("new area:", round(c.area, 4))

try:
    c.radius = -3
except ValueError as e:
    print("Caught:", e)
radius  : 5
diameter: 10
area    : 78.5398
new area: 314.1593
Caught: radius must be >= 0, got -3

Notice that area and diameter store nothing — they are computed fresh from _radius every time they are read. And because neither defines a setter, c.area = 10 would raise AttributeError: a read-only attribute, for free.

How it works under the hood

@property builds a descriptor and attaches it to the class, not the instance. From then on Python routes every access through it:

c.radius        →  Circle.radius.__get__(c, Circle)   →  return c._radius
c.radius = 10   →  Circle.radius.__set__(c, 10)       →  validate, then store

The real value lives in the plain _radius; the property is just the gate every read and write has to pass through. That is the whole trick.

When to reach for it — and when not to

Reach for @property when a value is derived (area), needs guarding (a non-negative radius), or should be computed lazily the first time it is asked for. Reserve it for those cases. Wrapping every ordinary attribute in a property “just in case” buys you nothing but indirection and a slower dot-access — the Pythonic default is a plain attribute until you have a concrete reason to intercept it.

Learn it properly Classes & Instances

Keep practising

All Python questions

Explore further

Skip to content