Python getters and setters implemented with the property decorator are the language's answer to a design tension that exists in every object-oriented codebase. On one hand, you want external code to interact with your objects through a clean, simple interface where reading and writing attributes uses the same dot notation as everything else in Python. On the other hand, you sometimes need to run code when an attribute is accessed or modified, to validate input, compute values on the fly, log changes, or maintain consistency between related attributes. Languages like Java resolve this tension with an explicit convention: private fields paired with public getX and setX methods. Python resolves it with properties, a mechanism that lets you define methods that look like attributes to the outside world but execute custom logic whenever they are accessed.
The @property decorator is the entry point for creating managed attributes. You write a method that returns a value, place @property on the line above it, and from that point forward external code accesses the method as if it were a plain attribute. No parentheses, no get prefix, no change to the calling syntax. If you later need to add validation on assignment, you define a setter method with the @name.setter decorator, and Python calls it automatically whenever external code assigns a value to the attribute. If you need to run cleanup code when an attribute is deleted, you define a deleter with @name.deleter. The property system is incremental: you can start with a plain attribute, convert it to a read-only property when you need computation, and add a setter when you need validation, all without changing a single line of code that uses the class.
The article on public, protected, and private members covered how Python uses naming conventions to communicate which attributes are internal and which are public. Properties work hand in hand with those conventions: you typically store the actual data in an underscored internal attribute and expose it through a property that controls how the data is accessed and modified. This pattern gives you the clean public interface of a plain attribute with the control and safety of explicit methods, and it is one of the features that makes Python class design feel both powerful and lightweight.
Read-only computed properties
The simplest use of @property is a read-only attribute whose value is computed from other attributes rather than stored independently. A class that stores a rectangle's width and height might provide an area property that multiplies the two dimensions on every access. The area is never stored; it is always calculated fresh, which means it can never become stale or inconsistent with the width and height. External code reads rect.area and receives the correct value regardless of how many times the dimensions have changed.
Here is a Rectangle class that demonstrates a computed read-only property alongside plain stored 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)The width and height attributes are plain public attributes that can be read and written freely. The area and perimeter are properties that compute their values on demand from the current width and height. If the width changes, the next read of area automatically reflects the new value. There is no risk of area becoming out of sync because it is never stored independently. The @property decorator makes the computed nature of these attributes invisible to the caller, who simply writes rect.area and receives a number.
Adding setters for validation and transformation
A property setter runs code every time a value is assigned to the property, which makes setters the ideal place for input validation. If an attribute must be a positive number, the setter checks the condition and raises a clear error if it is violated. If an attribute must be one of a fixed set of allowed values, the setter verifies membership before storing. The validation runs at the moment of assignment, which means invalid data never reaches the internal storage attribute.
The following Temperature class uses a property with both a getter and a setter to ensure that the internal value always represents a physically plausible temperature:
class Temperature:
def __init__(self, celsius):
self._celsius = None
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 = valueThe celsius property stores its value in the underscored internal attribute. The setter validates that the value is not below absolute zero before storing it.
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32The fahrenheit property is a read-only computed property that converts from the stored Celsius value on every access. External code interacts with the object through natural attribute syntax while the class maintains complete control over what values are accepted and how values relate to each other.
Notice that the constructor assigns to self.celsius, not to self._celsius. This is a deliberate pattern that routes the initial value through the setter, ensuring that the validation runs even during object creation. If a caller writes Temperature(-500), the constructor assigns -500 to self.celsius, the setter runs, the validation fails, and a ValueError is raised before the object is fully initialized. This pattern keeps validation in one place, the setter, rather than duplicating it in both the constructor and the setter.
Migrating from plain attributes to properties
One of the most practical benefits of properties is that they let you change your mind about an attribute's implementation without breaking existing code. A common scenario in real projects is that a class starts with a plain public attribute because the initial requirements are simple. Later, you discover that you need to validate the value, or compute it from other attributes, or log every change. If you had exposed the attribute through getter and setter methods, you would need to update every call site. Because you used a plain attribute with dot notation, you can replace it with a property of the same name, and every line of existing code continues to work without modification.
The migration is mechanical. The plain attribute assignment in the constructor changes to assign to the underscored internal name. The plain attribute name becomes a property with a getter that returns the internal value. If validation is needed, you add a setter. External code that reads and writes the attribute does not change at all. This forward compatibility is one of the reasons Python developers prefer properties over explicit getter and setter methods: properties let you defer design decisions about access control until you actually need them, without painting yourself into a corner.
Properties can also implement lazy loading, where a value is computed the first time it is accessed and then cached for subsequent accesses. A class that loads a large dataset from disk can store None as the internal attribute and compute the data on the first property access, replacing the None with the loaded data. Subsequent accesses return the cached data without repeating the expensive load. This pattern is invisible to the caller, who simply accesses the attribute and receives the data, unaware that the first access triggered a disk read while subsequent accesses are instantaneous.
When not to use properties
Properties are powerful, but they are not always the right choice. A plain public attribute is perfectly appropriate when the attribute is simple data with no validation requirements and no computation. Adding a property to an attribute that stores a simple string or number adds complexity without adding value. The caller sees the same dot notation either way, but the property introduces method call overhead and makes the class definition slightly harder to read at a glance.
Properties should also be avoided when the getter or setter performs expensive or surprising operations. A property getter that makes a network request, reads a file, or performs a complex database query violates the expectation that attribute access is fast and side-effect-free. If accessing a value is expensive, make it a regular method with a verb name like load_data or fetch_results so the caller understands that work is being done. The property decorator is for managing attribute access, not for disguising expensive operations as simple lookups.
The article on encapsulation in Python covers the broader principles of hiding implementation details, and properties are one of the primary tools for achieving that encapsulation. When you combine underscore-prefixed internal attributes with properties that control their access, you create classes that are safe to use, easy to change, and natural to read.
Rune AI
Key Insights
- The @property decorator turns a method into an attribute that runs code on access while looking like a plain attribute to external code.
- Define a setter with @name.setter to run validation or transformation when a value is assigned to the property.
- Properties let you migrate from plain attributes to managed attributes without changing any code that uses the class.
- A property without a setter is read-only; attempting to assign to it raises an AttributeError.
- Use properties for attributes that need validation, computation, or lazy loading; use plain attributes for simple data storage.
Frequently Asked Questions
What is the @property decorator in Python?
How do I create a read-only property in Python?
Can I add a property to an existing class without breaking code that already accesses the attribute directly?
Conclusion
The @property decorator is one of Python's most elegant features for class design. It lets you start with simple public attributes and add behavior later without breaking existing code. Properties keep your class interface clean by avoiding the getX and setX method naming pattern, and they integrate naturally with Python's dot-notation syntax that every developer already knows.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.