Properties: @property, getter and setter
The @property decorator lets you define methods that behave like attributes. This gives you control over how values are read, set, and deleted while keeping a clean, attribute-like interface.
Why Use Properties
In Python, you often want to add logic when getting or setting an attribute. Before properties, developers used getter and setter methods in other languages like Java:
class Circle:
def get_radius(self):
return self._radius
def set_radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
With properties, you use a natural attribute syntax while still controlling access:
class Circle:
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
# Now it looks like a simple attribute
circle = Circle()
circle.radius = 5 # Uses the setter
print(circle.radius) # Uses the getter
Python properties let you have the best of both worlds: the clean attribute syntax and the control of methods when you need it.
The Basics: Defining a Getter
The @property decorator converts a method into a getter. When you access obj.attribute, Python calls the decorated method instead of directly accessing the underlying attribute.
class Person:
def __init__(self, name):
self._name = name # Private attribute (by convention)
@property
def name(self):
"""Getter for the name attribute."""
return self._name
p = Person("Alice")
print(p.name) # Alice
The key convention: private attributes use a leading underscore (_name), and the property provides controlled access via the public name (name). This signals to other developers that they should use the property, not the private attribute directly.
Adding a Setter
Use the @attribute.setter decorator to define what happens when someone assigns to the attribute:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("Name must be a string")
if not value:
raise ValueError("Name cannot be empty")
self._name = value
p = Person("Alice")
p.name = "Bob" # Calls the setter
print(p.name) # Bob
p.name = "" # Raises ValueError
The setter lets you validate input, transform values, or trigger other actions when an attribute changes. This is where properties shine, keeping validation logic close to the data it protects.
Adding a Deleter
Similarly, @attribute.deleter defines what happens when someone deletes the attribute with del:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@name.deleter
def name(self):
print(f"Deleting {self._name}")
del self._name
p = Person("Alice")
del p.name # Prints "Deleting Alice" and removes _name
Deleters are less common but useful for cleanup tasks, logging when attributes are removed, or enforcing deletion policies.
Computed Properties
Properties can compute values on the fly based on other attributes:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
@property
def perimeter(self):
return 2 * (self.width + self.height)
r = Rectangle(4, 5)
print(r.area) # 20
print(r.perimeter) # 18
These are read-only properties since no setter is defined. Attempting to assign r.area = 100 raises an AttributeError.
Computed properties are great for derived data. The values are always up-to-date because they are recalculated each time you access them.
Read-Only Properties
To make a property read-only, simply omit the setter:
class Circle:
def __init__(self, radius):
self.radius = radius # Goes through setter
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(c.diameter) # 10
c.diameter = 20 # AttributeError: property diameter has no setter
This prevents external code from modifying computed or derived values. The user cannot accidentally overwrite a value that should be calculated automatically.
Property as Decorator Factory
You can also use property() directly for more control:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def _get_fahrenheit(self):
return self._celsius * 9/5 + 32
def _set_fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
fahrenheit = property(_get_fahrenheit, _set_fahrenheit)
This approach is useful when you want more control over the property creation or need to assign it dynamically. It is equivalent to using decorators but sometimes clearer for complex cases.
When to Use Properties
Properties are ideal when you need to validate values, ensure data meets certain criteria before storing, compute attributes that derive values from other attributes, implement lazy evaluation for expensive values only when needed, hide internal implementation details through encapsulation, and maintain API stability by starting with simple attributes then adding logic later without changing the API.
A common pattern is to start with simple public attributes. When you need validation or computed values later, convert them to properties. External code does not need to change since obj.x = value works the same either way.
This differs from languages like Java, where you must explicitly define getters and setters from the start. Python properties let you evolve your API without breaking existing code.
Property vs Getter Setter Methods
Properties give you the best of both worlds: the clean attribute syntax and the control of methods. This is a key part of Python is philosophy. You should not need to guess whether something is a simple attribute or a method call.
The rule of thumb is to use attributes by default. Convert to properties when you need behavior. Your users will thank you for the clean API.
See Also
-
property() - Built-in property function reference
-
Classes and Objects Tutorial - Object-oriented programming fundamentals
-
Decorators in Python - Understanding the decorator pattern
-
Python dataclasses - Data classes with automatic special methods