Back to Blog
Python

Python Property: Getters, Setters, and Validation

python property: Learn how to use Python's property decorator to control attribute access, add validation, and compute values with clean, readable code.

property decoratorgetters settersdata validationcomputed attributespython oop
Illustration of Python property decorator controlling attribute access with getter and setter methods.

The python property decorator lets you intercept attribute access on a class without exposing getter and setter methods to callers. It turns a method into an attribute, so you can add validation, compute values, or make attributes read-only while keeping the interface simple. Instead of writing obj.get_value() and obj.set_value(), you write obj.value and obj.value = new_value, and the underlying logic runs automatically.

The @property Decorator Syntax

The most common way to define a property is with the @property decorator. A method decorated with @property becomes the getter. To add a setter, you use the @<property_name>.setter decorator on a method with the same name. The same pattern works for a deleter with @<property_name>.deleter.

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 the internal attribute. The property celsius exposes it with validation. When you assign temp.celsius = 25, the setter runs and checks the value. When you read temp.celsius, the getter returns the stored value. This keeps the validation logic in one place and prevents the same checks from being duplicated across request handlers or other parts of the code.

Using property() Without Decorators

The property() built-in function is the underlying mechanism behind the decorator. You can call it directly with getter, setter, and deleter functions. This is useful when you need to define a property dynamically or when you prefer a more explicit style.

class Rectangle: def __init__(self, width, height): self._width = width self._height = height def get_area(self): return self._width * self._height def set_width(self, value): self._width = value width = property(get_width, set_width) area = property(get_area)

In this example, width is a property with a getter and setter, while area is read-only because it has only a getter. The property() function accepts up to four arguments: fget, fset, fdel, and doc. This form is equivalent to the decorator syntax, but the decorator is usually more readable when you have several properties in a class.

Adding Validation in Setters

A common use of properties is to validate data before it is stored. The setter runs every time the attribute is assigned, so you can enforce type checks, range limits, or business rules without scattering validation code across your application.

class Account: def __init__(self, balance): self._balance = balance @property def balance(self): return self._balance @balance.setter def balance(self, amount): if not isinstance(amount, (int, float)): raise TypeError("Balance must be a number") if amount < 0: raise ValueError("Balance cannot be negative") self._balance = amount

Now any assignment to account.balance is checked. If you later need to add a logging step or a database update, you can do it in the setter without changing the public interface. This is especially valuable when the class is used in multiple places, because the validation is centralized.

Computed Properties and Read-Only Access

Properties are not limited to storing and retrieving values. They can compute a value on the fly from other attributes. This keeps derived data consistent and avoids the need to manually update a cached field.

class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return 3.14159 * self.radius ** 2 @property def diameter(self): return self.radius * 2

area and diameter are computed each time they are accessed. If the radius changes, the computed values automatically reflect the new radius. This is preferable to storing area as a separate attribute, which could become stale.

To make a property read-only, simply omit the setter. Any attempt to assign to a read-only property raises an AttributeError. This is useful for values that should never change after initialization, such as a unique identifier or a creation timestamp.

The Deleter and Property Lifecycle

A property can also define a deleter, which runs when you use del obj.attribute. The deleter is useful for cleanup or to reset an attribute to a default state.

class Session: def __init__(self): self._token = None @property def token(self): return self._token @token.setter def token(self, value): self._token = value @token.deleter def token(self): self._token = None

Deleting a property attribute does not delete the underlying internal attribute unless you explicitly do so. In this example, del session.token resets the token to None, which might be the desired behavior for logging out. The deleter gives you control over what happens when a caller removes the attribute.

Performance and Overhead

Accessing a property is a method call, so it is slightly slower than accessing a plain attribute. In most applications, this overhead is negligible. However, in tight loops that access an attribute millions of times, the difference can become measurable. If you are optimizing a performance-critical path, consider whether a property is necessary.

Plain attributes are faster because they do not involve a function call. If you only need to store and retrieve a value without any validation or computation, a plain attribute is the better choice. Properties add flexibility at the cost of a small runtime penalty. You can mitigate this by caching computed values if the underlying data changes infrequently, but that adds complexity.

When to Use Properties vs Plain Attributes

Use a property when you need to control access to an attribute. Typical reasons include validation, lazy computation, or the need to change the internal representation without breaking external code. For example, if you store temperature in Celsius but want to expose it in Fahrenheit, you can use a property to convert on the fly.

Use a plain attribute when you have simple data storage and no need for control. A class that just holds a name and an email address does not benefit from properties. Adding properties to every attribute makes the code more verbose and slower without providing value.

The decision should be based on the actual requirements. If you are writing a class that will be used as a public API, properties allow you to evolve the internal implementation without changing the interface. If the class is internal to a module and you control all access, plain attributes are often sufficient.

Inheritance and Property Overriding

Properties behave like methods when it comes to inheritance. You can override a getter, setter, or deleter in a subclass by redefining the property. The override must use the same property name.

class Base: @property def value(self): return self._value @value.setter def value(self, v): self._value = v class Derived(Base): @property def value(self): return self._value * 2 @value.setter def value(self, v): self._value = v + 10

In Derived, reading value returns twice the stored value, and assigning value adds 10 before storing. This allows subclasses to modify behavior while keeping the same attribute interface. Note that you must redefine the entire property; you cannot override only the setter without also redefining the getter. This is a common source of confusion, so be aware of it when designing class hierarchies.

Properties are a fundamental tool in Python for creating clean, maintainable class interfaces. They let you add logic to attribute access without forcing callers to change how they interact with your objects. By using them judiciously, you can keep your code expressive and robust.

python property: Practical Usage and Code Examples | RYUSLOG DEV