Back to Blog
Python

Python Property Decorator: Controlling Attribute Access

Learn how the python property decorator transforms attribute access, enables computed values, and enforces validation without changing the class interface.

propertydecoratorattribute accesscomputed attributesvalidation
Illustration of a Python class attribute with a property decorator controlling access between an external caller and internal data.

The python property decorator turns a method into an attribute, letting you intercept reads, writes, and deletions with custom logic. It is one of the most direct ways to keep a class's public interface stable while changing the underlying behavior. Instead of writing separate get_foo() and set_foo() methods, you define a normal attribute and attach the logic to it.

The Basic Property Pattern

A property is created by decorating a method with @property. That method becomes the getter. To add a setter, you decorate a second method with the same name and @setter. A deleter uses @del.

class Temperature: def __init__(self, celsius): 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 = value

Here celsius is a property. Reading temp.celsius calls the getter, assigning temp.celsius = 25 calls the setter. The internal _celsius attribute remains private. This pattern is common because it allows you to add validation or computation later without forcing callers to change their code.

Computed Attributes and Derived State

Properties are often used for values that are derived from other attributes. A rectangle's area is a good example: it depends on width and height, but you don't need to store it separately.

class Rectangle: def __init__(self, width, height): self.width = width self.height = height @property def area(self): return self.width * self.height

Every access recomputes the area. That is fine when the calculation is cheap. If the computation is expensive, consider caching the result and invalidating the cache when dependencies change. A property itself does not provide caching; you have to manage that manually.

Validation and Invariants

The setter is the natural place to enforce invariants. When an object's state must satisfy a condition, the setter can reject invalid values before they corrupt the object. This keeps the validation logic in one place and prevents the same checks from being duplicated across request handlers or other call sites.

class Account: def __init__(self, balance): self._balance = balance @property def balance(self): return self._balance @balance.setter def balance(self, amount): if amount < 0: raise ValueError("Balance cannot be negative") self._balance = amount

Without the property, you would need to call a method like set_balance() and hope every caller remembers to validate. With the property, the validation is enforced on every assignment, including inside the class's own methods.

Read-Only and Controlled Attributes

If you define only a getter and no setter, the property becomes read-only. Assigning to it raises an AttributeError. This is useful for exposing a value that should never be changed from outside the class.

class User: def __init__(self, user_id): self._user_id = user_id @property def user_id(self): return self._user_id

Attempting user.user_id = 42 fails. The read-only behavior is enforced by the property descriptor protocol, not by a convention. This is stronger than relying on a leading underscore, which is only a naming convention.

Performance and Overhead

Every property access involves a method call, even if the method is trivial. In tight loops or hot paths, this overhead can become measurable. A property that simply returns a private attribute is slower than a direct attribute access because Python has to look up the descriptor, call the function, and then return the value.

If you are working with a class that is accessed millions of times per second, the overhead may matter. In most application code it does not. The maintainability benefit of a property often outweighs the microsecond cost. If you need the speed of a plain attribute but still want the option to add logic later, you can start with a plain attribute and migrate to a property when the need arises. Because the public interface is identical, the change is transparent to callers.

Inheritance and Name Mangling

Properties interact with inheritance in a way that can surprise developers. If a subclass overrides a getter but not the setter, the setter from the parent class is still used. This is because the property object is shared, and the setter is attached to the same descriptor.

class Base: @property def value(self): return self._value @value.setter def value(self, new_value): self._value = new_value class Child(Base): @property def value(self): return self._value * 2

In this example, Child overrides the getter but inherits the setter. That may or may not be what you want. If the subclass needs a different setter, you must redefine both methods. Also note that using _value inside a property method triggers name mangling if the method is defined inside a class with a double-underscore prefix, but for single underscore it is a normal attribute.

When to Use Property vs Plain Attributes

Use a property when you need to control access, compute a value, or enforce a rule. Use a plain attribute when the value is simple and no logic is required. A property is not a substitute for a method that performs a complex operation with side effects; if the operation is expensive or has side effects, a method with a verb name is clearer.

A property also helps when you are refactoring a class. If you initially expose self.name and later need to validate it, you can replace the attribute with a property without changing the external API. This is a key advantage: the property decorator lets you evolve the implementation without breaking callers.

Common Pitfalls

One frequent mistake is forgetting to use the underscore-prefixed attribute inside the property. If the getter returns self.value instead of self._value, it calls the property recursively, causing an infinite recursion. The same happens if the setter assigns to self.value.

Another pitfall is using a property for something that should be a method. If the computation takes a long time or has side effects, a property hides that from the caller. The caller expects attribute access to be cheap and side-effect free. If your property performs I/O or a database query, consider using a method instead.

Finally, remember that properties are class attributes, not instance attributes. They are stored on the class, and the descriptor protocol manages access. This is why you cannot override a property with an instance attribute of the same name; the property descriptor takes precedence.

python property decorator: Practical Usage and Code Examples | RYUSLOG DEV