Python Properties: Getters, Setters & Deleters
Python properties use the @property decorator to turn methods into attributes, providing clean attribute-like syntax while maintaining validation and control logic. This approach is more Pythonic than traditional getter/setter methods found in languages like Java. Properties enable you to manage attribute access elegantly, validate data on assignment, compute values on the fly, and delete attributes safely—all without breaking the simple, intuitive interface users expect.
Key Takeaways
@propertydecorator transforms a method into a read-only property, accessed likeobj.attrinstead ofobj.get_attr()@name.setterdecorator adds assignment validation, allowingobj.attr = valuewhile enforcing constraints@name.deleterdecorator controls what happens when an attribute is deleted withdel obj.attr- Computed properties calculate values dynamically from other attributes, staying synchronized without storing redundant data
Why Move Away from Traditional Getter/Setter Methods?
Traditional getter/setter methods, common in Java or C#, are verbose and feel unnatural in Python. Consider this approach:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible.")
self._celsius = value
t = Temperature(25)
t.set_celsius(30) # Verbose and clunky
print(t.get_celsius())
This pattern requires method calls for simple access, lacks the intuitive feel of direct attribute assignment, and if you later decide to add validation, you break code that accesses the attribute directly. Properties solve all three problems by allowing attribute syntax while preserving validation logic behind the scenes.
How Do You Create a Property with @property?
The @property decorator is placed above a method. The method name becomes the property name, and calling obj.property_name automatically invokes the method.
class Temperature:
def __init__(self, celsius):
self.__celsius = celsius # Private attribute
@property
def celsius(self):
"""Getter: called when accessing the property."""
print("Getting temperature...")
return self.__celsius
# Using the property
t = Temperature(25)
current_temp = t.celsius # Prints "Getting temperature..." and returns 25
print(f"The temperature is {current_temp}°C")
The @property decorator creates a read-only property. If you try to assign to it, Python raises an AttributeError:
t.celsius = 30 # AttributeError: can't set attribute 'celsius'
This is useful for computed values that should never be modified directly. For example, a full_name property that is always derived from first_name and last_name should be read-only.
How Do You Add a Setter with @property.setter?
To allow assignment while maintaining validation, define a setter method with the @<property_name>.setter decorator. The setter method must have the same name as the property.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # Uses the setter
@property
def celsius(self):
"""Getter for temperature in Celsius."""
return self.__celsius
@celsius.setter
def celsius(self, value):
"""Setter with validation logic."""
print(f"Setting temperature to {value}...")
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible.")
self.__celsius = value
# Using the property with a setter
t = Temperature(25) # Calls the setter in __init__
print(f"Temperature is {t.celsius}°C")
t.celsius = 30 # Calls the setter
print(f"Temperature is now {t.celsius}°C")
try:
t.celsius = -300 # Calls the setter, which raises an error
except ValueError as e:
print(f"Error: {e}")
Key points:
- The setter is called even in
__init__, allowing centralized validation - The setter receives the value being assigned as a parameter
- Validation happens before the private attribute is modified
- If validation fails, raise an exception (the attribute is not changed)
How Do You Add a Deleter with @property.deleter?
The @<property_name>.deleter decorator controls what happens when someone tries to delete the attribute using del obj.property.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self.__celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible.")
self.__celsius = value
@celsius.deleter
def celsius(self):
"""Deleter: called when del obj.celsius is executed."""
print("Deleting temperature attribute...")
del self.__celsius
# Using the deleter
t = Temperature(25)
print(f"Temperature: {t.celsius}°C")
del t.celsius # Calls the deleter
print("Temperature attribute has been deleted.")
try:
print(t.celsius) # Raises AttributeError
except AttributeError as e:
print(f"Error: {e}")
Deleters are useful when:
- You need to perform cleanup operations (logging, resource release)
- You want to prevent deletion by raising an exception
- You want to reset related attributes when one is deleted
How Do You Create Computed Properties?
Computed properties don't store data directly; they calculate their value from other attributes every time they are accessed. This ensures the value is always synchronized without requiring manual updates.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@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):
"""Computed property: converts Celsius to Fahrenheit."""
return (self.__celsius * 9/5) + 32
@property
def kelvin(self):
"""Computed property: converts Celsius to Kelvin."""
return self.__celsius + 273.15
# Using computed properties
t = Temperature(20)
print(f"{t.celsius}°C = {t.fahrenheit:.2f}°F = {t.kelvin:.2f}K")
t.celsius = 0
print(f"{t.celsius}°C = {t.fahrenheit:.2f}°F = {t.kelvin:.2f}K")
Computed properties are read-only by default (they have no setter). If you want to allow setting a computed property, you need to implement the setter to update the underlying attributes:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return (self.__celsius * 9/5) + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature using Fahrenheit."""
self.__celsius = (value - 32) * 5/9
What Are the Best Practices for Using Properties?
Best Practices:
- Use private or protected attributes (
__attror_attr) to store the actual data; let properties manage access - Keep getter logic simple; avoid side effects like printing or logging (unless necessary for debugging)
- Centralize validation in setters so all modifications go through the same checks
- Document properties thoroughly in docstrings explaining constraints and behavior
- Use computed properties for derived values that should always be in sync with base attributes
Anti-Patterns to Avoid:
- Avoid side effects in properties: Don't modify other attributes, write to files, or make network calls in getters/setters without documenting it clearly
- Don't over-use properties: If validation is not needed, direct attributes are fine
- Avoid expensive computations in getters: If a property's calculation is complex and called frequently, consider caching the result
- Don't change property behavior unexpectedly: A property should behave like an attribute; surprising behavior (raising exceptions, modifying global state) breaks user expectations
# Anti-pattern: side effects in getter
@property
def full_name(self):
logging.info(f"User accessed full_name") # Unnecessary logging
return f"{self.first_name} {self.last_name}"
# Better: just compute the value
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
How Do Properties Compare to Direct Attribute Access?
Properties provide several advantages over direct attributes but at a small runtime cost. Use this guide:
| Approach | When to Use | Trade-off |
|---|---|---|
Direct attribute (obj.attr = value) | Simple values, no validation needed | No control over modifications |
Property with getter only (@property) | Computed values, read-only data | Cannot modify after creation |
Property with setter (@property + @setter) | Data requiring validation | Slight overhead vs direct access |
Getter/setter methods (obj.get_attr()) | Complex logic, Java-style | Verbose, unPythonic syntax |
# Direct attribute: simple, but no validation
class Person:
def __init__(self, age):
self.age = age # Anyone can set any value
p = Person(25)
p.age = -10 # No validation; accepted
# Property with validation: Pythonic and safe
class Person:
def __init__(self, age):
self.age = age # Uses the setter
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("Age cannot be negative.")
self._age = value
p = Person(25)
p.age = -10 # Raises ValueError
Frequently Asked Questions
Can you have a property without a setter?
Yes, and it's common. A property with only a getter is read-only. This is useful for computed values or data that should not be modified after initialization.
What happens if you access a property that was deleted?
Accessing a deleted property raises an AttributeError. You can catch this exception to handle the missing attribute gracefully.
try:
print(t.celsius)
except AttributeError:
print("Temperature attribute has been deleted.")
Can properties be inherited by subclasses?
Yes, properties are inherited just like regular methods. Subclasses can override a parent's property by defining their own property with the same name.
How do you set a property in __init__ without infinite recursion?
When you call self.property_name = value in __init__, it calls the setter. The setter stores the value in a private attribute (like self.__property_name). The getter accesses that private attribute. This avoids recursion because the setter does not call itself.
Is there a performance penalty for using properties?
Yes, there is a small overhead: property access involves a function call, whereas direct attribute access is a direct lookup. For most applications, this overhead is negligible. If you're in a tight loop accessing a property millions of times, consider caching the value.