Property Decorators
@property and @setter — add validation and computed attributes while keeping the clean obj.attribute access style.
"@property lets you add validation and logic to attribute access without changing the interface. Code that reads obj.name still works — it just does more now."
— ShurAIThe Problem with Direct Attribute Access
Plain attributes have no protection. Anyone can set them to invalid values:
class Circle:
def __init__(self, radius):
self.radius = radius
c = Circle(5)
c.radius = -10 # valid Python, invalid geometry — no error!
@property — Read Access with Logic
@property turns a method into an attribute you read like a normal attribute but computed by a function:
import math
class Circle:
def __init__(self, radius):
self._radius = radius # _ prefix = "internal, use the property"
@property
def radius(self): # read as c.radius (no parentheses!)
return self._radius
@property
def area(self): # computed property — not stored
return math.pi * self._radius ** 2
c = Circle(5)
print(c.radius) # 5 — reads like an attribute
print(c.area) # 78.54... — computed on the fly
# c.radius = 10 ← raises AttributeError (no setter yet)
@name.setter — Adding Write Validation
Add a setter to allow writing to the property — with validation:
class Circle:
def __init__(self, radius):
self.radius = radius # uses the setter below automatically!
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError(f"Radius must be positive, got {value}")
self._radius = value # store in the private attribute
c = Circle(5)
c.radius = 10 # fine — uses setter
print(c.radius) # 10
c.radius = -3 # ValueError: Radius must be positive, got -3
x = obj.nameobj.name = xdel obj.nameself._radius not self.radius?
Inside the setter, self.radius = value would call the setter again — infinite recursion! Store the actual value in self._radius (with a leading underscore, meaning "internal use"). The property then exposes it cleanly as self.radius.
Real Example — Temperature Converter
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # triggers the setter
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
@property
def fahrenheit(self): # read-only computed property
return self._celsius * 9/5 + 32
@property
def kelvin(self):
return self._celsius + 273.15
t = Temperature(100)
print(f"{t.celsius}°C = {t.fahrenheit}°F = {t.kelvin}K")
# 100°C = 212.0°F = 373.15K
t.celsius = 0
print(f"{t.celsius}°C = {t.fahrenheit}°F")
# 0°C = 32.0°F
"The beauty of @property is transparency. Code that was accessing obj.radius directly still works exactly the same way — it just silently goes through your validation now."
— ShurAI🧠 Quiz — Q1
What does @property do to a method?
🧠 Quiz — Q2
If a class has @property for radius but no setter, what happens when you do c.radius = 5?
🧠 Quiz — Q3
Why do we store the value as self._radius inside the setter instead of self.radius = value?
🧠 Quiz — Q4
Which decorator is used to create a write handler for a property named name?