Mastering Python’s @property Decorator: Clean, Backward‑Compatible Getters & Setters
Mastering Python’s @property Decorator
Discover how Python’s built‑in @property decorator lets you write concise, maintainable getters and setters while keeping your API backward compatible.
Python’s @property decorator is a powerful tool for encapsulating attribute access. It enables you to define custom logic that runs whenever an attribute is read or written, without forcing callers to change their code. In the following sections we’ll walk through a real‑world example—handling temperature conversions—show how to enforce constraints, and explain why @property is the idiomatic solution.
A Class Without Getters and Setters
Suppose we need a simple class that stores temperature in Celsius and can convert to Fahrenheit:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
Using it is straightforward:
# Create a new object
human = Celsius()
# Set the temperature
human.temperature = 37
# Read the temperature
print(human.temperature)
# Convert to Fahrenheit
print(human.to_fahrenheit())
Output
37 98.60000000000001
The floating‑point result is expected due to binary representation of decimals. When you inspect human.__dict__, you’ll see that Python stores the value in a plain dictionary entry:
>>> human.__dict__
{'temperature': 37}
Adding Constraints with Getters and Setters
Real‑world objects often need validation. For example, temperatures cannot fall below absolute zero (-273.15 °C). One straightforward way is to hide the attribute and expose explicit getter/setter methods:
class Celsius:
def __init__(self, temperature=0):
self.set_temperature(temperature)
def to_fahrenheit(self):
return (self.get_temperature() * 1.8) + 32
# getter
def get_temperature(self):
return self._temperature
# setter
def set_temperature(self, value):
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible.")
self._temperature = value
Now the class enforces the lower bound, but any existing code that accessed obj.temperature must be updated to call obj.get_temperature() and obj.set_temperature(val). That’s a major refactor for large codebases.
Moreover, Python’s “private” variables are only a naming convention—_temperature can still be accessed directly, breaking encapsulation. This is where @property shines.
Using the property Class
Python offers a property class that ties getter and setter functions to an attribute name:
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
# getter
def get_temperature(self):
print("Getting value...")
return self._temperature
# setter
def set_temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273.15 is not possible")
self._temperature = value
# expose as a property
temperature = property(get_temperature, set_temperature)
With this setup, the following code behaves exactly like the original simple class, but with validation behind the scenes:
human = Celsius(37)
print(human.temperature) # triggers getter
print(human.to_fahrenheit()) # uses getter internally
human.temperature = -300 # triggers setter and raises ValueError
Output
Setting value... Getting value... 37 Getting value... 98.60000000000001 Setting value... Traceback (most recent call last): ... ValueError: Temperature below -273 is not possible
Notice how the code that accesses temperature remains unchanged; only the underlying implementation gained validation.
Tip: The actual data lives in _temperature; temperature is just a convenient, safe interface.
The @property Decorator
While the property class works, Python’s decorator syntax offers a cleaner, more readable way to achieve the same result. The decorator automatically creates the property object and attaches the getter and setter to the attribute name.
class Celsius:
def __init__(self, temperature=0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
@property
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter
def temperature(self, value):
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below -273 is not possible")
self._temperature = value
Usage remains the same:
human = Celsius(37)
print(human.temperature)
print(human.to_fahrenheit())
# Attempting to set an invalid temperature
try:
Celsius(-300)
except ValueError as e:
print(e)
Output
Setting value... Getting value... 37 Getting value... 98.60000000000001 Setting value... Traceback (most recent call last): ... ValueError: Temperature below -273 is not possible
Using @property keeps the namespace tidy, eliminates the need for separate getter/setter method names, and is the recommended pattern for most Python developers.
Python
- Mastering Python Operators: A Comprehensive Guide
- Python List Operations: Creation, Access, Modification, and Advanced Techniques
- Mastering Python Tuples: Creation, Access, and Advanced Operations
- Mastering Python Dictionaries: Creation, Manipulation, and Advanced Techniques
- Mastering Python Inheritance: Concepts, Syntax, and Practical Examples
- Mastering Operator Overloading in Python: A Practical Guide
- Mastering Python’s @property Decorator: Clean, Backward‑Compatible Getters & Setters
- Build a Remote Temperature Sensor with Raspberry Pi and Python – Step‑by‑Step Guide
- Raspberry Pi BMP085: Accurate Temperature & Pressure Readings via I2C
- Build a ThingSpeak Temperature Monitor with Raspberry Pi & BrickPi