Properties — `@property`, `@setter`, `@deleter` for controlled access
WHY do properties exist?
WHY not just expose the attribute directly? Because then nothing stops someone writing
account.balance = -9999. You lose your invariants (rules that must always hold).
WHY not just use get_x()/set_x() methods? Because then every call site must write
obj.get_x(). If you started with obj.x, switching breaks all that code. Properties let you
keep the clean obj.x syntax and gain control.
WHAT is a property, mechanically?
The decorators are just sugar:
class C:
@property
def x(self): # this is property(x).fget
return self._x
@x.setter
def x(self, value): # returns a NEW property with fset filled in
self._x = value
@x.deleter
def x(self): # fills in fdel
del self._xHOW Python resolves obj.x (the derivation)
When you access obj.x, Python does not just look in obj.__dict__. The attribute
lookup follows the descriptor protocol:
- Look through
type(obj).__mro__for a namex. - If found AND it's a data descriptor (defines
__set__or__delete__) → callx.__get__(obj, type(obj)). This wins over instance__dict__. - Else check
obj.__dict__['x']. - Else if a non-data descriptor (only
__get__) was found → use it. - Else
AttributeError.

Worked Example 1 — Validation (Temperature)
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # goes THROUGH 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): # computed, read-only
return self._celsius * 9/5 + 32| Step | Code | Why this step? |
|---|---|---|
| 1 | self.celsius = celsius in __init__ |
Use the property even during construction, so validation runs from birth. |
| 2 | store in self._celsius |
The backing field uses a different name (_celsius) so the setter doesn't call itself forever. |
| 3 | fahrenheit has no setter |
Read-only computed value — derived from celsius, so setting it directly would be ambiguous. |
t = Temperature(25)
t.fahrenheit # 77.0 (computed, no stored field)
t.celsius = -300 # ValueErrorWorked Example 2 — Read-only + deleter (Account)
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance # no setter ⇒ can't reassign
@balance.deleter
def balance(self):
print("closing account")
self._balance = 0| Step | Code | Why this step? |
|---|---|---|
| 1 | no @balance.setter defined |
Makes acc.balance = 5 raise AttributeError → balance can only change via deposit/withdraw methods. |
| 2 | @balance.deleter resets to 0 |
del acc.balance becomes a controlled "close" action, not a memory wipe. |
acc = Account(100)
acc.balance # 100
acc.balance = 50 # AttributeError: can't set attribute
del acc.balance # prints "closing account", balance -> 0Worked Example 3 — Lazy / cached computation
class Circle:
def __init__(self, r):
self._r = r
self._area = None
@property
def area(self):
if self._area is None: # compute once, then cache
print("computing...")
self._area = 3.141592653589793 * self._r ** 2
return self._area
@property
def radius(self):
return self._r
@radius.setter
def radius(self, v):
self._r = v
self._area = None # invalidate cache!Why this step? Setting radius must reset _area, otherwise area returns a stale
cached value. Properties give you the hook to keep derived data consistent.
Recall Feynman: explain it to a 12-year-old
Imagine a vending machine with a slot that says "Money". From outside it looks like a simple
slot — you just push money in. But inside, a little guard checks: is this a real coin? enough
money? If not, it spits it back. A property is that guard. From the outside machine.money = 5
looks like dropping a coin in a slot, but a hidden checker (the setter) decides whether to accept it.
A read-only property is a slot where you can only look at what's inside (@property) but the
guard won't let you push anything in (no @setter).
Flashcards
What does @property turn a method into?
obj.x runs the method while looking like plain attribute access.Why store data in self._x instead of self.x inside a setter?
self.x would re-trigger the setter → infinite recursion; _x is a separate backing field.What error does obj.x = v raise when only @property (no setter) is defined?
What syntax adds a setter to property x?
@x.setter above a method named x (same name).Why does a property override an instance __dict__ entry of the same name?
__set__), so descriptor lookup wins over instance dict.What does @x.deleter control?
del obj.x — lets you clean up instead of removing the attribute blindly.What principle says callers shouldn't care if obj.x is stored or computed?
When caching with a property, what must the setter of the source value do?
Is a read-only property's underlying value immutable?
_x.What three functions can a property hold?
Connections
- Encapsulation and Access Modifiers — properties enforce encapsulation in Python's "we're all adults" way.
- Descriptors __get__ __set__ __delete__ — the lower-level mechanism
propertyis built on. - Getters and Setters in Java vs Python — same goal, cleaner syntax.
- functools.cached_property — built-in caching property.
- Class vs Instance Attributes — property lives on the class; backing field lives on the instance.
- Invariants and Class Design — why validation in setters protects object state.
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Dekho, @property ka basic funda ye hai: bahar se to lagta hai aap ek normal attribute use
kar rahe ho — obj.x — par andar chupke se ek function chal raha hota hai. Isse hum controlled
access dete hain. Matlab agar koi account.balance = -5000 set karne ki koshish kare, to
setter validation kar ke usse rok sakta hai. Plain attribute me ye control nahi milta.
Tin pieces hain: @property (value padhne/get karne ke liye), @x.setter (value set karte
waqt check/validate karne ke liye), aur @x.deleter (del obj.x ke time clean-up ke liye).
Sabse bada trap ye hai — setter ke andar self.x = value mat likhna, kyunki wo dobara
setter ko hi call karega aur infinite recursion ho jayega. Iske bajaye self._x = value
likho — underscore wala backing field, taaki recursion na ho.
Technically, property ek data descriptor hai (uske paas __set__ hota hai), isliye jab
Python obj.x dhoondta hai to wo instance ke __dict__ se bhi pehle property ko pakadta hai.
Isi wajah se aapka setter guarantee se chalta hai. Aur ek mast use-case: computed/read-only
values — jaise fahrenheit jo celsius se calculate hota hai, ya area jo radius se. Agar
source change ho to setter me cache ko None kar do, warna purani stale value aati rahegi.
Yaad rakhna: property ka asli fayda Uniform Access Principle hai — caller ko pata bhi nahi
chalega ki obj.x stored data hai ya calculate ho raha hai, aur baad me upgrade karne par
purana code todna nahi padta.