property()
The controlled-attribute pattern — expose a method as if it were a plain attribute.
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(fgetfget — The getter function. Called when the attribute is READ. Takes self, returns the value.type: callable · default: None=None, fsetfset — The setter function. Called when the attribute is ASSIGNED. Takes self and value. Without one, the property is read-only.type: callable · default: None=None, fdelfdel — The deleter function. Called when the attribute is DELETED. Takes self.type: callable · default: None=None, docdoc — Docstring. If omitted, uses fget's docstring.type: str · default: None=None)
→ property
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| fget | callable | no (None) | The getter function. Called when the attribute is READ. Takes self, returns the value. |
| fset | callable | no (None) | The setter function. Called when the attribute is ASSIGNED. Takes self and value. Without one, the property is read-only. |
| fdel | callable | no (None) | The deleter function. Called when the attribute is DELETED. Takes self. |
| doc | str | no (None) | Docstring. If omitted, uses fget's docstring. |
Return value
property — A 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 attribute3. Validated setter
a.value = -1
Returns
ValueError: age must be non-negative4. Delete via property
del t.celsius
Returns
runs deleter5. Docstring from getter
help(Circle.area)
Returns
shows getter docstringPitfalls
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.