Python - Python Property Decorators and Managed Attributes
Introduction
In Python, a class normally allows its attributes to be accessed and modified directly. For example, if a class has an attribute called age, we can write person.age = 25 to assign a value and print(person.age) to retrieve it. Although this approach is simple, there are situations where we need more control over how an attribute is read, modified, or deleted.
Python provides the @property decorator for this purpose. A property allows a method to be accessed like a normal attribute. This makes it possible to add validation, calculations, formatting, or other logic while keeping a simple and readable syntax for users of the class.
Property decorators are especially useful when implementing managed attributes, where the class controls how its internal data is accessed and changed.
What Is a Property?
A property is a special kind of attribute that is controlled by methods. Instead of directly exposing an internal variable, a class can provide a property through which the variable is accessed.
Consider a simple class:
class Student:
def __init__(self, name):
self.name = name
student = Student("Rahul")
print(student.name)
Here, name is a normal instance attribute. It can be accessed and changed directly.
student.name = "Anil"
There is no validation or additional logic involved.
With a property, we can control what happens when the attribute is accessed.
class Student:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
Now the name() method behaves like an attribute:
student = Student("Rahul")
print(student.name)
The important point is that we do not write:
student.name()
Instead, we use:
student.name
The @property decorator converts the method into a property.
Why Use the @property Decorator?
Properties are useful when an attribute requires additional control.
For example, suppose a student's age should never be negative. With a normal attribute, Python allows this:
student.age = -10
A property can prevent invalid values.
class Student:
def __init__(self, age):
self._age = age
@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
Now:
student = Student(20)
print(student.age)
student.age = 25
print(student.age)
The setter validates the value before storing it.
If we attempt:
student.age = -5
Python raises a ValueError.
This makes properties useful for maintaining valid object states.
Getter in Python Properties
The method decorated with @property is commonly called the getter.
Example:
class Employee:
def __init__(self, salary):
self._salary = salary
@property
def salary(self):
return self._salary
The getter is executed whenever we access:
employee.salary
It internally executes:
return self._salary
The underscore in _salary is a naming convention indicating that the attribute is intended for internal use.
The property provides a controlled public interface:
employee.salary
while the actual stored value is:
employee._salary
Setter in Python Properties
A setter defines what should happen when a property is assigned a new value.
The @property_name.setter decorator is used to create a setter.
class Employee:
def __init__(self, salary):
self._salary = salary
@property
def salary(self):
return self._salary
@salary.setter
def salary(self, value):
if value < 0:
raise ValueError("Salary cannot be negative")
self._salary = value
Now:
employee = Employee(50000)
employee.salary = 60000
print(employee.salary)
When this statement executes:
employee.salary = 60000
Python calls the setter automatically.
The setter checks the value and then stores it in _salary.
Getter and Setter Together
A property can have both a getter and a setter.
class Product:
def __init__(self, price):
self._price = price
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value <= 0:
raise ValueError("Price must be greater than zero")
self._price = value
Using the class:
product = Product(100)
print(product.price)
product.price = 150
print(product.price)
The getter retrieves the value, while the setter controls modifications.
This provides a clean interface:
product.price
product.price = 150
Instead of exposing internal implementation details.
Property Validation
One of the most common applications of properties is validation.
Consider a bank account:
class BankAccount:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
The class ensures that the balance cannot become negative through the property.
account = BankAccount(5000)
account.balance = 6000
print(account.balance)
An invalid assignment is rejected:
account.balance = -1000
This type of validation is valuable in applications where data must satisfy specific rules.
Read-Only Properties
A property does not necessarily need a setter.
For example:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius * self.radius
The area property can be read:
circle = Circle(5)
print(circle.area)
But there is no setter for area.
Therefore, users should not assign a new value to it:
circle.area = 100
A read-only property is useful when a value should be calculated from other attributes rather than independently stored.
Calculated Properties
Properties can also represent calculated values.
For example:
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
@property
def area(self):
return self.length * self.width
@property
def perimeter(self):
return 2 * (self.length + self.width)
Now:
rectangle = Rectangle(10, 5)
print(rectangle.area)
print(rectangle.perimeter)
There is no need to store area or perimeter separately. They are calculated whenever they are requested.
This reduces unnecessary duplicated data.
Property Deleter
Python also allows a property to define what happens when an attribute is deleted.
The @property_name.deleter decorator is used for this purpose.
class User:
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("Deleting name")
del self._name
Now:
user = User("Rahul")
print(user.name)
del user.name
When del user.name is executed, the property deleter is called.
Complete Property Example
The following example demonstrates getter, setter, and deleter together:
class Student:
def __init__(self, name, marks):
self._name = name
self._marks = marks
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not value:
raise ValueError("Name cannot be empty")
self._name = value
@property
def marks(self):
return self._marks
@marks.setter
def marks(self, value):
if value < 0 or value > 100:
raise ValueError("Marks must be between 0 and 100")
self._marks = value
Using the class:
student = Student("Rahul", 85)
print(student.name)
print(student.marks)
student.name = "Anil"
student.marks = 90
print(student.name)
print(student.marks)
Here, the properties ensure that invalid names and marks are not accepted.
Difference Between a Normal Attribute and a Managed Attribute
A normal attribute is directly accessed and modified:
class Student:
def __init__(self, marks):
self.marks = marks
There is no validation:
student.marks = -50
A managed attribute uses a property:
class Student:
def __init__(self, marks):
self._marks = marks
@property
def marks(self):
return self._marks
@marks.setter
def marks(self, value):
if value < 0:
raise ValueError("Marks cannot be negative")
self._marks = value
Now the class has control over how marks is changed.
This is the main idea behind managed attributes.
Why Use an Underscore?
You will frequently see code such as:
self._name
and:
@property
def name(self):
return self._name
The underscore distinguishes the internal storage variable from the public property.
The property is:
name
The internal attribute is:
_name
This avoids a naming conflict.
If we attempted:
@property
def name(self):
return self.name
the property would call itself repeatedly and eventually cause a recursion error.
Using _name avoids this problem.
Advantages of Property Decorators
Properties provide several important benefits.
Data Validation
They allow a class to reject invalid values before storing them.
Encapsulation
They hide implementation details and provide controlled access to internal data.
Read-Only Data
A property without a setter can prevent users from directly changing a calculated or protected value.
Computed Values
Properties can calculate values dynamically instead of storing duplicate information.
Cleaner Syntax
A method can be accessed using attribute syntax:
object.value
rather than:
object.get_value()
Easier Code Evolution
A class can initially expose a normal-looking attribute and later introduce validation or calculation through a property without changing how calling code accesses it.
Property Decorator vs Traditional Getter and Setter Methods
Before properties, developers commonly used methods such as:
class Student:
def get_marks(self):
return self._marks
def set_marks(self, value):
self._marks = value
The caller would use:
student.get_marks()
student.set_marks(90)
With properties, the interface becomes:
student.marks
student.marks = 90
The property approach is often more natural because the operation looks like normal attribute access while still allowing the class to execute additional logic.
Important Points to Remember
The @property decorator creates a getter for an attribute.
The @property_name.setter decorator creates a setter.
The @property_name.deleter decorator creates a deleter.
A property without a setter can effectively be used as a read-only attribute.
Properties are commonly used for validation, encapsulation, calculated values, and controlled access to internal attributes.
The internal variable commonly uses a leading underscore, such as _age, _salary, or _name.
Properties allow developers to maintain a simple public interface while keeping control over the internal implementation of a class.
Conclusion
Python property decorators provide a powerful way to create managed attributes. Instead of allowing unrestricted access to an object's internal data, properties let a class decide how values are retrieved, modified, calculated, or deleted.
The key components are the getter using @property, the setter using @attribute.setter, and the optional deleter using @attribute.deleter. Together, they make classes more reliable, maintainable, and easier to use while supporting important object-oriented programming principles such as encapsulation and controlled data access.