Properties

The @property decorator lets you expose computed or validated attributes through plain attribute syntax, so callers write obj.value instead of obj.get_value().

A property is a class attribute that intercepts access, assignment, and deletion through descriptor machinery while still looking like an ordinary attribute to the caller. It lets you start with a simple public attribute and later add validation or computation without breaking every piece of code that reads or writes it.

Why Properties Exist

In many languages you write getter and setter methods from the start, because you can never predict whether a field will need validation later, and changing a public field into get_x()/set_x() breaks every caller. Python takes the opposite default: start with a plain public attribute (self.radius = radius). If you later need validation or computed behavior, convert that attribute to a property. The attribute-access syntax (obj.radius) never changes, so existing code keeps working.

This is often summarized as 'preserving API compatibility': Python properties let the *implementation* change from a stored field to a computed one without changing the *interface* callers use.

Basic Syntax: @property

Decorate a method with @property to turn it into a getter. Callers access it without parentheses, exactly like a plain attribute.

radius mirrors a stored value; area is a fully computed attribute with no backing field at all. Both are accessed identically.

class Circle:
    def __init__(self, radius: float) -> None:
        self._radius = radius

    @property
    def radius(self) -> float:
        return self._radius

    @property
    def area(self) -> float:
        return 3.14159265 * self._radius ** 2


c = Circle(2.0)
print(c.radius)  # 2.0 (no parentheses)
print(c.area)    # computed on every access, 12.566...

Adding a Setter

A property defined with only @property is read-only: assigning to it raises AttributeError. To allow assignment, define a second method with the same name decorated with @<name>.setter. This is where validation logic typically lives.

Notice __init__ assigns self.radius = radius, not self._radius = radius — this routes the initial value through the same validation as any later assignment.

The setter must be decorated with @radius.setter — the name after @ refers to the property object created by @property, not a hardcoded keyword. Both methods share the same name (radius) by convention and by requirement.

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius  # goes through the setter below

    @property
    def radius(self) -> float:
        return self._radius

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


c = Circle(3.0)
c.radius = 5.0     # runs the setter, validates, stores
print(c.radius)
c.radius = -1      # raises ValueError

Adding a Deleter

A property can also intercept del obj.attr via @<name>.deleter. This is less common than getters/setters but useful for cleanup or invalidation logic.

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius

    @property
    def radius(self) -> float:
        return self._radius

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

    @radius.deleter
    def radius(self) -> None:
        print("clearing radius")
        del self._radius


c = Circle(4.0)
del c.radius   # prints 'clearing radius', removes self._radius

Computed (Derived) Attributes

A property need not wrap a stored field at all — it can compute a value from other attributes every time it's read. This keeps derived values from drifting out of sync with the data they depend on, since there is nothing to forget to update.

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    @property
    def area(self) -> float:
        return self.width * self.height

    @property
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)


r = Rectangle(3, 4)
print(r.area, r.perimeter)  # 12 14
r.width = 10
print(r.area)  # 40 -- recomputed automatically, no stale cache

Validation on Assignment

The most common real-world use of setters is enforcing invariants at the moment a value is assigned, rather than discovering an invalid state much later when it causes a confusing failure elsewhere.

Every path that sets balance -- __init__, direct assignment, or a future subclass -- goes through the same checks.

class Account:
    def __init__(self, balance: float) -> None:
        self.balance = balance

    @property
    def balance(self) -> float:
        return self._balance

    @balance.setter
    def balance(self, value: float) -> None:
        if not isinstance(value, (int, float)):
            raise TypeError("balance must be numeric")
        if value < 0:
            raise ValueError("balance cannot be negative")
        self._balance = value

Read-Only Properties

Defining only a getter (no setter) makes an attribute read-only from outside the class. This is a common pattern for values that should be computed once and never reassigned, such as an object's identity or a value derived entirely from constructor arguments.

class Employee:
    def __init__(self, first: str, last: str) -> None:
        self._first = first
        self._last = last

    @property
    def full_name(self) -> str:
        return f"{self._first} {self._last}"


e = Employee("Ada", "Lovelace")
print(e.full_name)
e.full_name = "New Name"  # AttributeError: can't set attribute

How Properties Work: the Descriptor Protocol

property is a built-in class implementing the descriptor protocol: it defines __get__, __set__, and __delete__. When you write @property above a method, Python replaces the class attribute with a property object built from that method as its getter. property(fget, fset, fdel) is the underlying constructor; the decorator syntax is sugar over calling it and chaining .setter/.deleter, which each return a *new* property object with that piece filled in.

This is exactly what the decorator form desugars to; seeing it written out demystifies what @property and @radius.setter are doing.

Because property lives on the *class*, not the instance, it applies uniformly to every instance and is looked up via the normal attribute-lookup machinery before instance __dict__ (data descriptors take priority) -- this is why obj.radius = 5 runs the setter instead of just inserting 'radius' into obj.__dict__.

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

    def _get_radius(self):
        return self._radius

    def _set_radius(self, value):
        self._radius = value

    radius = property(_get_radius, _set_radius)  # equivalent, without decorators

Practical Usage Patterns

Common, idiomatic uses of properties include: unit conversion (store one representation, expose another), lazy computation with caching, enforcing constraints, and providing a stable public name while an internal representation changes.

Only celsius is stored; fahrenheit is a fully synthetic two-way view onto it.

class Temperature:
    def __init__(self, celsius: float) -> None:
        self.celsius = celsius

    @property
    def fahrenheit(self) -> float:
        return self.celsius * 9 / 5 + 32

    @fahrenheit.setter
    def fahrenheit(self, value: float) -> None:
        self.celsius = (value - 32) * 5 / 9


t = Temperature(0)
print(t.fahrenheit)  # 32.0
t.fahrenheit = 212
print(t.celsius)     # 100.0

Edge Cases

A property with a setter but a getter that raises, or vice versa, is legal but confusing -- keep getter/setter/deleter behaviorally consistent. Properties are resolved on the class where they're defined; a subclass overriding only the getter loses the base class's setter unless it redefines the whole property (commonly by calling super() inside a new property block, or by reusing the base property object).

Overriding only the getter in Sub silently drops the setter unless it's explicitly re-declared, because 'value' in Sub is now a brand-new property object.

class Base:
    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        self._value = v


class Sub(Base):
    @property
    def value(self):
        return super().value * 2  # reuses base getter logic

    @value.setter
    def value(self, v):
        Base.value.fset(self, v)  # must re-attach base setter explicitly

Mental Model and Quick Reference

Think of a property as a pair (or triple) of hidden method calls wearing an attribute-access disguise: obj.x calls the getter function, obj.x = v calls the setter function with v, and del obj.x calls the deleter. The disguise is complete -- callers cannot tell, from syntax alone, whether an attribute is a plain stored field or a property.

@property             -> defines the getter (obj.x)
@x.setter             -> defines the setter (obj.x = v)
@x.deleter             -> defines the deleter (del obj.x)
property(fget,fset,fdel) -> equivalent non-decorator constructor
Getter only            -> read-only attribute

Key Takeaways

@property turns a method into something accessed with plain attribute syntax, with no parentheses.

@name.setter and @name.deleter add write and delete interception; without a setter the property is read-only.

Properties let you start with a public attribute and add validation or computation later without changing the calling code.

A property with no setter raises AttributeError on assignment; a subclass must redefine the whole property, not just one accessor, to keep inherited behavior.

Under the hood, property is a descriptor implementing __get__/__set__/__delete__ on the class.

class C: @property def x(self): return self.x # infinite recursion class C: @property def x(self): return self._x

The getter must read from a differently named backing attribute (conventionally _x); returning self.x calls the getter again, recursing until RecursionError.

class C: def __init__(self, v): self._v = v # bypasses validation @property def v(self): return self._v @v.setter def v(self, val): if val < 0: raise ValueError self._v = val class C: def __init__(self, v): self.v = v # goes through the setter @property def v(self): return self._v @v.setter def v(self, val): if val < 0: raise ValueError self._v = val

Assigning directly to the private backing field in __init__ skips validation entirely; assign through the public property name instead.

class Sub(Base): @property def value(self): return super().value * 2 # forgot the setter -- it's gone now class Sub(Base): @property def value(self): return super().value * 2 @value.setter def value(self, v): Base.value.fset(self, v)

Redefining a property's getter in a subclass creates a brand-new property object that has no setter unless you explicitly add one back.

What is the difference between accessing a property and calling a regular method?

A property is accessed with plain attribute syntax (obj.x, no parentheses); a method requires an explicit call (obj.x()). Under the hood a property getter is still a function call, but the syntax hides that.

Why does a property with only @property (no setter) raise an error on assignment?

Because the property object's fset is None; attempting obj.x = v calls the (missing) setter, and Python raises AttributeError: can't set attribute.

Why is it idiomatic in __init__ to write self.radius = radius rather than self._radius = radius when radius is a property with a setter?

Assigning to self.radius routes the initial value through the setter's validation logic, so construction and later assignment enforce the same invariants; assigning directly to self._radius bypasses that validation.

What problem does converting a public attribute to a property solve that adding get_x()/set_x() methods from the start does not?

It preserves the existing attribute-access API: code written as obj.x = 5 continues to work unchanged even after obj.x becomes a validated or computed property, whereas switching to get_x()/set_x() would require rewriting every caller.

Why does overriding only the getter of an inherited property in a subclass lose the inherited setter?

@property on the subclass's method creates an entirely new property object bound to that name; it doesn't inherit fset/fdel from the base class's property object unless the subclass explicitly reattaches them.

What Python built-in class implements the mechanism behind the @property decorator, and which protocol does it use?

The built-in property class, which implements the descriptor protocol (__get__, __set__, __delete__).

When would you prefer a fully computed property (no backing field at all) over a stored attribute with a setter?

When the value is always derivable from other attributes (e.g. area from width and height) -- a computed property guarantees it can never go stale, whereas a stored value could drift out of sync if the source attributes change.

Write a Stack-like Inventory class with a property count that is read-only and always reflects len(self._items).

class Inventory: def __init__(self): self._items = [] def add(self, item): self._items.append(item) @property def count(self) -> int: return len(self._items) inv = Inventory() inv.add("bolt") inv.add("nut") print(inv.count) # 2 count is a read-only computed property (getter only, no setter) that recomputes len(self._items) on every access, so it can never fall out of sync with the underlying list.

What does this print?

caught: AttributeError -- P.x has only a getter, so assignment raises AttributeError, which the except clause catches and reports.

Write a Person class where age is a property that raises ValueError for negative values and allows 0 or positive integers.

class Person: def __init__(self, age: int) -> None: self.age = age @property def age(self) -> int: return self._age @age.setter def age(self, value: int) -> None: if value < 0: raise ValueError("age cannot be negative") self._age = value p = Person(30) p.age = 0 # fine try: p.age = -5 except ValueError as e: print(e)

The setter validates every assignment (including the one inside __init__, since __init__ writes self.age, not self._age), rejecting negative values with ValueError while allowing zero and positive integers.

class P:
    @property
    def x(self):
        return 10

p = P()
try:
    p.x = 20
except AttributeError as e:
    print("caught:", type(e).__name__)