Python Property Setter: Syntax and Validation
How the python property setter works: syntax, validation, derived values, read-only properties, and common recursion mistakes.
A python property setter is the decorated method that runs when code assigns a value to a property-backed attribute. It is declared with @<name>.setter directly below a property method, and it gives you a single place to validate, transform, or reject input before that value reaches the underlying instance state.
Property Setter Syntax and the @setter Decorator
The simplest form of a property setter stores a value in a backing attribute:
class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): self._celsius = value
The @property decorator turns celsius into a property object. That object exposes a .setter method, and @celsius.setter returns a new property with the setter installed. The setter method must use the same name as the getter, and it must accept exactly one argument besides self, which is the assigned value.
Assignment now goes through the setter:
t = Temperature(20) t.celsius = 25
Reading t.celsius still calls the getter, so the public interface is unchanged. Only the write path is intercepted.
Validation Logic in the Setter
The most common reason to add a setter is validation. Keeping the check inside the setter means every assignment path is covered, including assignments made inside other methods of the class:
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 cannot be below absolute zero") self._celsius = value
The constructor still assigns directly to self._celsius, which bypasses the setter. If you want the invariant enforced during construction as well, call self.celsius = celsius inside __init__ instead of writing to the backing attribute directly.
Computed or Derived Values in Setters
A setter does not have to store the assigned value verbatim. It can transform input before writing it, which is useful when a property represents a derived quantity:
class Rectangle: def __init__(self, width, height): self._width = width self._height = height @property def area(self): return self._width * self._height @area.setter def area(self, value): self._height = value / self._width
Assigning to area now updates the height so that the area invariant holds. This works, but it makes the setter's behavior less obvious to callers. Reserve derived-value setters for cases where the transformation is natural, such as converting between units, and document the side effect clearly.
Read-Only Properties and the Missing Setter
If a property has no setter, assignment raises AttributeError:
class Reading: def __init__(self, value): self._value = value @property def value(self): return self._value
r = Reading(42) r.value = 10 # AttributeError: can't set attribute
Omitting the setter is a deliberate way to expose a read-only attribute without losing the ability to change it internally through the backing field. This is the standard pattern for immutable public values that still need internal mutation during initialization or deserialization.
The Deleter and the Full Property Lifecycle
A property can also define a deleter, which runs when del obj.attribute is called:
class Temperature: def __init__(self, celsius): self._celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): self._celsius = value @celsius.deleter def celsius(self): del self._celsius
The deleter is rarely needed, but it matters when the backing attribute holds a resource that must be released explicitly. The three decorators form a complete lifecycle:
| Method | Decorator | Runs when |
|---|---|---|
| getter | @property | reading the attribute |
| setter | @<name>.setter | assigning a value |
| deleter | @<name>.deleter | deleting the attribute |
Common Mistakes: Recursion and Ordering
The most frequent bug with a python property setter is naming the backing attribute the same as the property:
class Broken: @property def value(self): return self.value @value.setter def value(self, new_value): self.value = new_value
Inside the setter, self.value = new_value calls the setter again, which recurses until RecursionError. The backing attribute must have a different name, conventionally prefixed with an underscore. The same mistake in the getter causes infinite recursion on read.
Ordering also matters: the setter decorator references the property object, so the getter must be defined before the setter in the class body. Defining the setter first raises NameError because value does not yet exist as a property object.
Performance and Maintainability Considerations
Every assignment through a property setter adds a method call on top of the attribute write. In a tight loop that performs millions of assignments, direct access to self._celsius is measurably cheaper, but for typical application code the overhead is negligible. Profile before optimizing; replacing a property with direct attribute access for performance reasons is rarely justified.
The real value of a setter is maintainability. Validation, unit conversion, and logging live in one method instead of being duplicated at every call site. When the invariant changes, you edit one place. That single point of control is the main reason to prefer a property setter over exposing the backing attribute directly, even when the setter currently does nothing but store the value.
The setter is inherited by subclasses, and it can be overridden to extend validation. Because the setter operates on the instance, it also works with any code that relies on the public attribute interface, such as serialization libraries or configuration loaders. Keep the backing attribute private and route all external writes through the property to preserve those guarantees.