property()

The controlled-attribute pattern — expose a method as if it were a plain attribute.

Built-in function / decoratorPython 2.2+
Common call
@property def name(self): return self._name
Returns
a descriptor — accessed like an attribute, backed by a method
Replaces
plain public attributes when you need validation, laziness, or a computed value
Watch out
setter and deleter use @name.setter / @name.deleter, NOT @property
property(fgetfgetThe getter function. Called when the attribute is READ. Takes self, returns the value.type: callable · default: None=None, fsetfsetThe setter function. Called when the attribute is ASSIGNED. Takes self and value. Without one, the property is read-only.type: callable · default: None=None, fdelfdelThe deleter function. Called when the attribute is DELETED. Takes self.type: callable · default: None=None, docdocDocstring. If omitted, uses fget's docstring.type: str · default: None=None)
property

Parameters

NameTypeRequiredDescription
fgetcallableno (None)The getter function. Called when the attribute is READ. Takes self, returns the value.
fsetcallableno (None)The setter function. Called when the attribute is ASSIGNED. Takes self and value. Without one, the property is read-only.
fdelcallableno (None)The deleter function. Called when the attribute is DELETED. Takes self.
docstrno (None)Docstring. If omitted, uses fget's docstring.

Return value

propertyA descriptor that, when accessed on an instance, calls the getter function. When assigned to, calls the setter. When deleted, calls the deleter. Attribute-style access on the outside; method logic on the inside.

Common patterns

Read-only property
Just @property — no setter means assignment raises.
class Circle:
    def __init__(self, r):
        self._r = r

    @property
    def area(self):
        return 3.14159 * self._r ** 2
Validated setter
Guard invariants at the attribute boundary.
class Age:
    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        if v < 0:
            raise ValueError("age must be non-negative")
        self._value = v
Computed / lazy property
Value derived from other attributes; often cached with functools.cached_property.
class Rect:
    @property
    def area(self):
        return self.width * self.height
Full getter / setter / deleter
All three phases of the attribute lifecycle.
class Temperature:
    @property
    def celsius(self):
        return self._c
    @celsius.setter
    def celsius(self, v): self._c = v
    @celsius.deleter
    def celsius(self): del self._c

Examples

1. Read-only access
c = Circle(3) c.area
Returns
28.274...
2. Read-only rejects assign
c.area = 42
Returns
AttributeError: can't set attribute
3. Validated setter
a.value = -1
Returns
ValueError: age must be non-negative
4. Delete via property
del t.celsius
Returns
runs deleter
5. Docstring from getter
help(Circle.area)
Returns
shows getter docstring

Pitfalls

1. Read-only by default — no setter means assignment fails
A common gotcha for users adding @property to a plain attribute. Without an explicit setter, the attribute becomes read-only and assignment raises AttributeError.
Assignment blocked
@property
def x(self): return self._x
# obj.x = 1
AttributeError: can't set attribute
Add a setter
@x.setter
def x(self, v): self._x = v
assignment works
2. Setter uses @name.setter, NOT @property
A common typo. The setter must be decorated with @name.setter (where name is the property name), not @property.
Second @property
@property
def x(self, v): ...
confusing — replaces the getter
Use .setter
@x.setter
def x(self, v): ...
attaches the setter
3. Property lives on the CLASS, not the instance
Assigning `instance.__dict__["x"]` would shadow the property. Descriptors work at the class level; instance-dict tricks defeat them.
Instance shadow
obj.__dict__["x"] = 1
obj.x
1 # shadows the property
Do not touch __dict__
obj.x = 1   # goes through the setter
controlled
4. Attribute lookups become method calls — cost is not zero
Every read/write goes through Python. For hot loops, either cache the value (functools.cached_property) or expose the plain attribute.
Expensive in loop
for _ in range(10**6):
    x = obj.area   # runs the getter each time
slow
Cache
from functools import cached_property
@cached_property
def area(self): ...
computed once per instance

When to use

Use it
  • Attribute access that needs validation on write
  • Computed / derived values presented as attributes
  • Read-only exposure of internal state
  • Migrating a plain attribute to controlled access without breaking callers
Reach for something else
  • Plain attributes → do not wrap for the sake of wrapping
  • Very hot code paths → consider cached_property or a plain attribute
  • Setters that do a lot of work → users expect assignment to be cheap
  • When you need cls or no self → classmethod / staticmethod

Notes

Complexity
O(1) descriptor invocation plus the getter/setter body
Return
A property descriptor attached to the class
CPython impl
Objects/descrobject.c :: property_descr_get/set/delete
Memory
Small descriptor per property, per class
Thread-safe
Depends on the getter/setter bodies

FAQ

Do not define a setter. Just @property with the getter — assignment will raise AttributeError.

History

2.2
property introduced with new-style classes.
2.6
setter, getter, deleter decorator forms added.
3.8
functools.cached_property added.