Course Progress76%
🍎 Python Advanced Python Topic 76 / 100
⏳ 8 min read

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."

— ShurAI

The Problem with Direct Attribute Access

Plain attributes have no protection. Anyone can set them to invalid values:

python — unprotected attribute
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:

python
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:

python
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
@property
Getter. Called on read:
x = obj.name
@name.setter
Setter. Called on write:
obj.name = x
@name.deleter
Deleter. Called on:
del obj.name
Why store as self._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

python
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?