Properties
The @property decorator transforms methods into attribute-like access,
giving you the clean syntax of obj.value while keeping the control of getter/setter
methods. This is Python's preferred way to implement managed attributes with validation,
computation, or lazy loading.
Why Properties?
Properties let you:
- Add validation without changing how users access the attribute
- Create computed values that look like attributes
- Make read-only attributes
- Refactor internal implementation without breaking the public API
Property Basics
The @property decorator turns a method into a "getter" that's accessed like an
attribute - no parentheses needed.
Click Run to execute your code
Start Public, Add Properties Later
Python's philosophy: start with simple public attributes. If you later need validation or
computation, convert to a property - the interface stays the same! Users still write
obj.value, not obj.get_value().
Property Setters
Add a setter to allow assignment (obj.value = x) with validation. Use the
@property_name.setter decorator.
Click Run to execute your code
Setter Must Have the Same Name
The setter decorator must be @property_name.setter and the method name must match
the property name. The internal storage should use a different name (typically with underscore:
_property_name).
Computed Properties
Properties can compute values from other attributes - like area from width
and height. These are typically read-only (no setter).
Click Run to execute your code
@cached_property (Python 3.8+)
For expensive computations that don't change, use @cached_property from functools.
It computes once on first access and caches the result. Unlike @property, it doesn't
recompute on each access.
Practical Property Patterns
Real-world uses of properties: backward-compatible refactoring, dependent properties, lazy loading, and access tracking.
Click Run to execute your code
Properties for API Stability
Properties let you change implementation details without changing the public interface.
Users write the same obj.value whether it's stored directly, computed,
validated, or lazily loaded.
Common Mistakes
1. Using the same name for property and storage
# Wrong - infinite recursion!
class Person:
@property
def name(self):
return self.name # Calls itself!
@name.setter
def name(self, value):
self.name = value # Calls itself!
# Correct - use different name for storage
class Person:
@property
def name(self):
return self._name # Underscore prefix
@name.setter
def name(self, value):
self._name = value
2. Forgetting to use @property_name.setter
# Wrong - this creates a new property!
class Circle:
@property
def radius(self):
return self._radius
@property # Wrong! Should be @radius.setter
def radius(self, value):
self._radius = value
# Correct
class Circle:
@property
def radius(self):
return self._radius
@radius.setter # Correct decorator
def radius(self, value):
self._radius = value
3. Not using the setter in __init__
# Problem - validation bypassed in __init__
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # Bypasses validation!
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
# Better - use property in __init__ for validation
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # Uses setter - validates!
4. Making properties do too much work
# Bad - expensive operation on every access
class Report:
@property
def data(self):
# This runs a database query EVERY time!
return self.database.fetch_all_records()
# Better - cache expensive operations
class Report:
@property
def data(self):
if self._data is None:
self._data = self.database.fetch_all_records()
return self._data
# Or use @cached_property (Python 3.8+)
5. Properties with side effects
# Bad - getter with surprising side effects
class Counter:
@property
def value(self):
self._access_count += 1 # Side effect!
return self._value
# Getters should be "pure" - no unexpected changes
# If you need to track access, be explicit about it
Exercise: Temperature Converter
Task: Create a Temperature class with properties for multiple units.
Requirements:
celsiusproperty with getter and setter (validates >= -273.15)fahrenheitproperty with getter and setter (converts to/from celsius)kelvinproperty - read-only getter (celsius + 273.15)__str__showing all three units
Click Run to execute your code
Show Solution
class Temperature:
def __init__(self, celsius=0):
self.celsius = celsius # Uses setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self.celsius = (value - 32) * 5/9
@property
def kelvin(self):
return self._celsius + 273.15
# No kelvin setter - read-only!
def __str__(self):
return f"{self.celsius}°C / {self.fahrenheit}°F / {self.kelvin}K"
# Test
t = Temperature(0)
print(t) # 0°C / 32.0°F / 273.15K
t.celsius = 100
print(t) # 100°C / 212.0°F / 373.15K
t.fahrenheit = 68
print(t) # 20.0°C / 68.0°F / 293.15K
Summary
@propertycreates a getter - access method like an attribute@name.settercreates a setter with validation@name.deleterhandlesdel obj.name- Store internal value with underscore:
_name(not same as property name!) - Computed properties derive values from other attributes
- Use
@cached_propertyfor expensive one-time computations
What's Next?
Now that you understand properties, learn about Abstract Classes - using
the abc module to define interfaces that subclasses must implement, ensuring
consistent APIs across related classes.
Enjoying these tutorials?