Learn how to use the @property decorator in Python to define attributes that behave like data but execute code, enabling validation and computed values without breaking existing interfaces.
What it is
In many languages, you expose internal state via public fields or write explicit getter/setter methods. In Python, we prefer simple attributes (e.g., obj.x) for direct access. However, when you need logic—such as validation, calculation, or lazy loading—you can upgrade an attribute into a property using the @property decorator. A property allows you to define a method that looks and acts like an attribute. This maintains backward compatibility: code that reads obj.area does not need to change if area becomes a property later.
Related terms include descriptors (the underlying mechanism), getters, setters, and deleters.
Why it matters
- Data Validation: Prevent invalid states by checking values during assignment.
- Computed Attributes: Provide derived values (like area from width/height) without storing redundant data.
- API Stability: Change internal implementation details without forcing clients to update their code.
- Laziness: Calculate expensive results only when accessed.
Syntax or steps
To create a read-only property, decorate a method with @property. To make it writable, add a setter decorated with @name.setter. For deletion, use @name.deleter.
- Define the instance variables (usually private, prefixed with
_). - Create a method named after the desired attribute.
- Add the
@propertydecorator above the method definition. - (Optional) Add
@method_name.setterto handle assignment.
Example
class Rectangle:
def __init__(self, w, h):
self._w = w
self._h = h
@property
def area(self):
"""Compute area dynamically."""
return self._w * self._h
@property
def width(self):
return self._w
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self._w = value
# Usage
r = Rectangle(5, 10)
print(r.area) # Output: 50 (acts like an attribute)
r.width = 8 # Triggers setter validation
print(r.area) # Output: 80
try:
r.width = -5 # Raises ValueError
except ValueError as e:
print(e)
The area property calculates the result on every access. The width property uses a setter to ensure the value remains positive before updating the internal _w variable.
Common mistakes
- Recursion in Setters: Assigning to the property name inside its own setter causes infinite recursion. Always assign to the underlying private variable (e.g.,
self._w = value, notself.w = value). - Forgetting the Underscore: If you don't prefix internal storage with
_, you might accidentally overwrite the property descriptor itself. - Overusing Properties: Do not use properties for trivial getters that just return a stored value. Use them only when logic is required.
- Performance Assumptions: Properties execute code on every access. Avoid heavy computations in frequently accessed properties unless caching is implemented.
When to use it
| Approach | Best For | Trade-off |
|---|---|---|
| Simple Attribute | Direct data storage with no logic. | No validation or computation possible. |
Method Call (get_area()) |
Explicit actions or complex calculations. | Breaks "attribute-like" syntax; verbose. |
Property (@property) |
Validation, computed values, API stability. | Slight overhead; requires careful naming. |
Practice
Guided Exercise: Create a Temperature class that stores Celsius internally. Add a property fahrenheit that converts and returns the value. Add a setter for fahrenheit that updates the internal Celsius value.
Challenge: Modify the Rectangle example to cache the area so it is only recalculated if width or height changes. Hint: Use a flag or compare current dimensions against cached ones.
Quick check
Q: Why do we typically store data in self._value instead of self.value when defining a property named value?
A: Storing in self.value would trigger the property's setter again, causing infinite recursion. Using self._value accesses the raw attribute directly.
Summary
Python properties allow you to attach logic to attribute access while maintaining clean syntax. They are essential for validating input, computing derived values, and evolving classes without breaking client code. Use them judiciously where behavior differs from simple data storage.