Skip to main content

Properties: Getters, Setters, and Deleters

We've seen how to use private attributes (__name) and public methods (get_name(), set_name()) to enforce encapsulation. This pattern, common in languages like Java or C#, is often called the "getter/setter" pattern.

While this works perfectly well in Python, it's not very "Pythonic." The Python community prefers a more elegant and seamless way to manage attribute access: properties.

The @property decorator lets you turn a class method into a "getter" for an attribute, while also allowing you to define "setter" and "deleter" logic. This lets you use the simple, clean syntax of direct attribute access (obj.name) while keeping all the power and safety of your validation logic.


📚 Prerequisites

You should understand encapsulation and the use of private attributes to protect data.


🎯 Article Outline: What You'll Master

In this article, you will learn:

  • The Problem with Getters/Setters: Why obj.get_value() and obj.set_value() can be verbose.
  • The @property Decorator: How to create a read-only "getter" property.
  • The @<name>.setter Decorator: How to add a "setter" to your property to validate incoming data.
  • The @<name>.deleter Decorator: How to add a "deleter" to control an attribute's deletion.
  • Computed Attributes: How to use properties to create attributes that are calculated on the fly.

🧠 Section 1: From Getters/Setters to Properties

Let's start with a Temperature class that uses the traditional getter/setter pattern.

# old_way.py
class Temperature:
def __init__(self, celsius):
self._celsius = celsius # A "protected" attribute

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)
print(t.get_celsius())

This is functional, but the method calls t.set_celsius(30) and t.get_celsius() feel a bit clunky. We're used to direct attribute access like t.celsius = 30. Properties let us have the best of both worlds.


💻 Section 2: The @property Decorator

The @property decorator is placed above a method. This turns the method into a read-only property. The method name becomes the attribute name.

Let's refactor our Temperature class.

# property_example.py

class Temperature:
def __init__(self, celsius):
# We will store the actual data in a "private" attribute
self.__celsius = celsius

@property
def celsius(self):
"""This is the 'getter' method for the celsius property."""
print("Getting value...")
return self.__celsius

# --- Let's use it ---
t = Temperature(25)

# Access it like a normal attribute. This automatically calls the getter method.
current_temp = t.celsius
print(f"The temperature is {current_temp}°C")

# What happens if we try to set it?
try:
t.celsius = 30
except AttributeError as e:
print(f"\nError trying to set: {e}")

Output:

Getting value...
The temperature is 25°C

Error trying to set: can't set attribute 'celsius'

We now have a read-only property. Accessing t.celsius is clean and intuitive, but it's still running our getter method under the hood. And because we haven't defined a "setter," the attribute is protected from modification.


🛠️ Section 3: Adding a Setter and Deleter

To allow modification, we need to define a setter method. We do this by creating another method with the same name as the property and decorating it with @<property_name>.setter.

A deleter works the same way, using @<property_name>.deleter.

# full_property_example.py

class Temperature:
def __init__(self, celsius):
# The setter is called even in the constructor!
self.celsius = celsius

@property
def celsius(self):
"""The getter for the temperature in Celsius."""
return self.__celsius

@celsius.setter
def celsius(self, value):
"""The setter with validation logic."""
print("Setting value...")
if value < -273.15:
raise ValueError("Temperature below absolute zero is not possible.")
# The actual data is stored in the private attribute
self.__celsius = value

@celsius.deleter
def celsius(self):
"""The deleter for the celsius property."""
print("Deleting value...")
del self.__celsius

# --- Let's use the full property ---
t = Temperature(25) # 'Setting value...' is printed here!

t.celsius = 30 # Calls the setter
print(f"Temperature is now {t.celsius}°C") # Calls the getter

try:
t.celsius = -300 # Calls the setter, which raises an error
except ValueError as e:
print(f"Error: {e}")

del t.celsius # Calls the deleter

🚀 Section 4: Computed Attributes

Another powerful use for properties is to create attributes that are calculated from other attributes. Let's add a Fahrenheit property to our Temperature class.

class Temperature:
def __init__(self, celsius):
self.celsius = celsius # Uses the celsius.setter

# ... (celsius property, setter, deleter from before) ...

@property
def fahrenheit(self):
"""A computed attribute that calculates Fahrenheit on the fly."""
return (self.__celsius * 9/5) + 32

# --- Let's use the computed property ---
t = Temperature(20)
print(f"{t.celsius}°C is equal to {t.fahrenheit:.2f}°F")

t.celsius = 0
print(f"{t.celsius}°C is equal to {t.fahrenheit:.2f}°F")

The fahrenheit attribute doesn't store any data itself; it calculates its value every time it's accessed, ensuring it's always in sync with the celsius value.


✨ Conclusion & Key Takeaways

This is the final article in our introduction to OOP! Properties are the Pythonic way to manage attribute access, providing a clean interface while maintaining the safety of encapsulation. They allow you to write code that is simple to use but has complex validation and logic hidden behind the scenes.

Let's summarize the key takeaways:

  • Properties are Methods that Act Like Attributes: They provide a clean syntax for attribute access.
  • @property: Creates a read-only getter.
  • @<name>.setter: Creates a setter to control and validate modification.
  • @<name>.deleter: Creates a deleter to control deletion.
  • Use a "Private" Name for Storage: The actual data should be stored in a protected (_) or private (__) attribute that the property methods manage.

➡️ Next Steps

Congratulations on completing this entire series on the fundamentals of Object-Oriented Programming! You now have the tools to design and build well-structured, encapsulated, and reusable classes.

In the next series, we will explore "Advanced OOP Concepts in Python," starting with Inheritance, a powerful mechanism for creating specialized classes based on existing ones.

Happy coding!