2.1.7OOP Fundamentals

Properties — `@property`, `@setter`, `@deleter` for controlled access

1,874 words9 min readdifficulty · medium

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

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

  1. Look through type(obj).__mro__ for a name x.
  2. If found AND it's a data descriptor (defines __set__ or __delete__) → call x.__get__(obj, type(obj)). This wins over instance __dict__.
  3. Else check obj.__dict__['x'].
  4. Else if a non-data descriptor (only __get__) was found → use it.
  5. Else AttributeError.
Figure — Properties — `@property`, `@setter`, `@deleter` for controlled access

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  # ValueError

Worked 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 -> 0

Worked 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?
A read-accessor so obj.x runs the method while looking like plain attribute access.
Why store data in self._x instead of self.x inside a setter?
Assigning to 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?
AttributeError: can't set attribute / property has no setter.
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?
It's a data descriptor (defines __set__), so descriptor lookup wins over instance dict.
What does @x.deleter control?
What happens on 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?
The Uniform Access Principle.
When caching with a property, what must the setter of the source value do?
Invalidate the cache (reset the cached field to None) so derived values aren't stale.
Is a read-only property's underlying value immutable?
No — only external reassignment is blocked; internal methods can still change _x.
What three functions can a property hold?
fget, fset, fdel (get, set, delete).

Connections

  • Encapsulation and Access Modifiers — properties enforce encapsulation in Python's "we're all adults" way.
  • Descriptors __get__ __set__ __delete__ — the lower-level mechanism property is 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

motivates

is a

builds

accumulate into

defines __set__

defines __delete__

intercepts

is data descriptor so

validates to keep

Uniform Access Principle

property object

Data descriptor

Decorator sugar

fget - @property

fset - @setter

fdel - @deleter

obj.x lookup

Wins over instance __dict__

Preserves invariants

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.

Go deeper — visual, from zero

Test yourself — OOP Fundamentals

Connections