What are properties (`@property`), and when should you use them over public attributes?

6 minintermediateooppropertyencapsulation

Quick Answer

`@property` turns a method into an attribute-like accessor, letting you run code on `get`/`set`/`delete` while keeping the call site looking like plain attribute access (`obj.value`, not `obj.get_value()`). Use it to add validation, computed/derived values, or lazy evaluation **without breaking the public API** — you can start with a plain attribute and convert it to a property later without touching any calling code.

Detailed Answer

Basic usage

class Circle:
    def __init__(self, radius):
        self._radius = radius

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

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("radius must be positive")
        self._radius = value

    @property
    def area(self):                 # read-only, computed property
        return 3.14159 * self._radius ** 2

c = Circle(5)
c.radius          # 25 -> looks like an attribute, actually calls the getter
c.radius = 10      # calls the setter, validates
c.area             # computed on the fly, no setter defined -> read-only
c.area = 100        # AttributeError: can't set attribute

Callers write c.radius, not c.get_radius() — the property mechanism is invisible from the outside.

Why this beats Java/C#-style getters/setters written from day one

Python convention starts with a plain public attribute (self.radius = radius, no property at all). If validation or computed logic is needed later, converting it to a @property doesn't change the public API — obj.radius still works exactly the same for every existing caller. Writing get_radius()/set_radius() methods from the start (the Java convention) locks callers into method-call syntax even when no validation is needed, and later can't be un-done without breaking callers.

Read-only vs computed properties

A property with only a getter (like area above) is effectively read-only — attempting c.area = 100 raises AttributeError. This is a clean way to expose a derived value without letting callers set it directly and risk it going out of sync with radius.

@property vs __slots__ and descriptors

property is itself implemented as a descriptor (it defines __get__/__set__/__delete__) — it's the built-in, most common special case of the more general descriptor protocol used for custom attribute-access behavior (see also: descriptors question).

Interview-ready summary: @property lets an attribute-looking access run arbitrary code (validation, computed values, logging) while keeping the calling code's syntax unchanged. The idiomatic Python pattern is to start with plain attributes and only introduce a property when you actually need that behavior — never write boilerplate getters/setters preemptively.

Related Resources