Intuition The one-sentence core
A class attribute is shared by every object of the class (one copy, lives on the class), while an instance attribute is owned by a single object (its own copy, lives on self).
Intuition Why this distinction exists
Think of a class as a cookie cutter and objects as cookies .
Some facts are true of all cookies the cutter makes → store them once on the cutter (class attribute). Example: "all dogs belong to species Canis familiaris ".
Some facts differ per cookie → store them on each cookie (instance attribute). Example: each dog has its own name.
Putting shared data on every instance would waste memory and make updates painful (you'd have to change 1000 copies). Putting per-object data on the class would make all objects collide on the same value.
Class attribute : a variable defined in the class body (directly under class X:), not inside any method. Stored in the class's namespace (X.__dict__). One shared copy.
Instance attribute : a variable assigned via ==self.name = value== (usually in __init__). Stored in that object's namespace (obj.__dict__). One copy per object .
class Dog :
species = "Canis familiaris" # class attribute (shared)
def __init__ (self, name):
self .name = name # instance attribute (per object)
Intuition The lookup rule — everything follows from this
When you read obj.x, Python searches in this order:
the instance obj.__dict__
then the class type(obj).__dict__
then base classes (MRO)
When you write obj.x = ..., Python always creates/updates an attribute on the instance — it never modifies the class.
This single asymmetry (read = search up, write = always on instance) explains every tricky behaviour below.
Worked example Reading then writing a class attribute
class Dog :
species = "Canis familiaris"
def __init__ (self, name): self .name = name
a = Dog( "Rex" )
b = Dog( "Fido" )
print (a.species) # 'Canis familiaris'
print (b.species) # 'Canis familiaris'
Why this step? Neither instance has species in its own __dict__, so lookup falls through to the class → both see the same shared value.
a.species = "Wolf" # WRITE → makes an instance attribute on `a`
print (a.species) # 'Wolf' (instance copy shadows class)
print (b.species) # 'Canis familiaris' (b still falls through to class)
print (Dog.species) # 'Canis familiaris' (class untouched!)
Why this step? Writing a.species follows the write rule : it created a brand-new key in a.__dict__. Now a's read finds it first (step 1), so the class value is shadowed , not overwritten. b and Dog are unaffected.
Dog.species = "Doggo" # change the class → all who DON'T shadow see it
print (a.species) # 'Wolf' (a shadows)
print (b.species) # 'Doggo' (b falls through to updated class)
Why this step? Editing on the class changes the single shared copy; every instance without its own species sees the new value live.
Worked example Shared mutable list bites everyone
class Dog :
tricks = [] # class attribute — ONE shared list
def __init__ (self, name): self .name = name
def add_trick (self, t): self .tricks.append(t) # NO assignment!
a, b = Dog( "Rex" ), Dog( "Fido" )
a.add_trick( "roll" )
print (b.tricks) # ['roll'] 😱 Fido learned Rex's trick
Why this happens? self.tricks.append(...) is a read (finds the shared class list) followed by mutating that object . There is no self.tricks = ... assignment , so no instance copy is ever created. Both dogs share one list.
The fix — make it a per-object instance attribute:
class Dog :
def __init__ (self, name):
self .name = name
self .tricks = [] # fresh list per instance (an assignment!)
def add_trick (self, t): self .tricks.append(t)
Why the fix works? Each __init__ runs self.tricks = [], an assignment → creates a separate list in each instance's __dict__.
obj.x = 5 changes the class attribute"
Why it feels right: you read obj.x and got the class value a second ago, so writing it "should" hit the same place. Symmetry is seductive.
The fix: read and write are asymmetric. Write always lands on the instance. To change the class value, write on the class : ClassName.x = 5.
Common mistake "Use a class attribute for the default, then just mutate it"
Why it feels right: "It's a default list, everyone starts empty, so a shared empty list is fine." Looks DRY and clean.
The fix: mutation (.append, .update) shares state across all instances. Use class attributes only for immutable / truly-shared data; per-object mutable state belongs in __init__ via self..
Common mistake "Class attributes are like static variables that can't change"
Why it feels right: other languages call them static const.
The fix: in Python a class attribute is mutable and reassignable via ClassName.attr = ...; "class" just means where it lives , not immutable .
Recall Feynman: explain to a 12-year-old
Imagine a classroom . There's one whiteboard at the front that everybody shares — if the teacher writes "Homework: page 5" on it, every student sees the same message. That's a class attribute .
Each student also has their own notebook . What I write in my notebook is mine alone ; yours stays blank. That's an instance attribute .
Tricky part: if I want to "change the homework," I can either (a) write a new note in my own notebook (now only I see my version — the whiteboard is unchanged), or (b) walk up and rewrite the whiteboard (everyone's homework changes). Python's self.x = ... is always "write in my notebook"; ClassName.x = ... is "rewrite the whiteboard".
"Self for self, Class for class."
self.x = ... → your own copy (WRITE = instance, always ).
Want sharing/change-for-all? Go to the Class name.
And: "Mutable default? Move it to __init__."
Where is a class attribute stored vs an instance attribute?
What does obj.x = 5 do if x was previously only a class attribute?
Why do two instances accidentally share a [] class attribute but not an int class attribute reassigned per instance?
Where is a class attribute physically stored? In the class's namespace (ClassName.__dict__); one shared copy for all instances.
Where is an instance attribute stored? In that object's own namespace (obj.__dict__); a separate copy per instance.
In what order does Python resolve obj.x on a READ? Instance __dict__ first, then the class, then base classes via the MRO.
What does obj.x = value always do, regardless of whether x was a class attribute? Creates/updates x on the INSTANCE; it never modifies the class.
What is "shadowing"? When an instance attribute hides a class attribute of the same name during lookup, because the instance is searched first.
Why does a mutable class attribute (e.g. tricks = []) get shared across instances? Because .append() is a read+mutate of the single shared object; without an assignment no per-instance copy is ever created.
How do you fix a shared-mutable-default bug? Assign a fresh object per instance inside __init__ (self.tricks = []).
How do you change a class attribute for ALL instances at once? Reassign it on the class itself: ClassName.attr = new_value.
After a.species = "Wolf", what does Dog.species return? The unchanged original class value — a only added a shadowing instance attribute.
Are class attributes immutable in Python? No; "class" refers to where they live, not immutability — they can be reassigned via the class name.
OOP Fundamentals
Classes and objects
The self parameter
__init__ constructor
Inheritance and MRO
Namespaces and scope
Mutable vs immutable objects
Intuition Hinglish mein samjho
Dekho, simple analogy lo: ek class ek cookie-cutter jaisa hai aur objects us cutter se bani cookies. Kuch cheezein sab cookies me same hoti hain — jaise "sab dogs ki species Canis familiaris hai". Aisi shared cheez ko hum class attribute banate hain, jo sirf ek baar class ke andar likhi jaati hai aur sabhi objects use share karte hain. Lekin kuch cheezein har object ki apni hoti hain — jaise har dog ka apna name. Yeh instance attribute hai, jo self.name = ... se banta hai aur har object ke paas alag copy hoti hai.
Sabse important rule yaad rakho: jab tum koi attribute read karte ho (obj.x), Python pehle instance me dhoondta hai, na mile to class me. Lekin jab tum write karte ho (obj.x = 5), woh hamesha instance par banta hai — class ko kabhi nahi badalta. Isi asymmetry se saari magic samajh aati hai. Agar tum a.species = "Wolf" likhoge to sirf a ka apna copy banega (shadowing), class aur baaki objects untouched rahenge. Agar sabko change karna hai to class par likho: Dog.species = "...".
Ek bada trap hai mutable class attribute ka. Maan lo tricks = [] class me hai aur tum self.tricks.append("roll") karte ho. Yeh assignment nahi hai — yeh ek shared list ko mutate kar raha hai, isliye saare dogs ki tricks me woh add ho jaata hai! Fix simple hai: list ko __init__ me self.tricks = [] karke banao, taaki har object ki apni alag list ho.
Mantra yaad rakho: "Self for self, Class for class" aur "Mutable default? Move it to __init__." Bas yeh do line interview aur real code dono me bachayegi.