Intuition Why this page exists
The parent note told you the rule (class = cutter, object = cookie). This page drags that rule through every situation it can meet : two objects, zero objects, shared class data, shadowing, identity traps, mutable-default traps, and an exam twist. If you can predict the output of all of these, you truly understand the blueprint/instance split.
Throughout, we reuse one blueprint. Read it once — every example builds on it.
class Dog :
species = "Canis" # CLASS attribute (one copy, shared)
def __init__ (self, name, age): # constructor: runs once per object
self .name = name # INSTANCE attribute (own copy)
self .age = age
def bark (self):
return f " {self .name } says Woof!"
Definition The three words we will test all page
Class — the description. Stored once. Holds the code and any class attributes.
Instance / object — a real thing built by calling Dog(...). Has its own instance attributes.
Identity — which memory cell an object lives in. a is b is True only when a and b are literally the same cell.
Every case this topic can throw at you falls into one cell below. The examples that follow are labelled with the cell they cover.
Cell
Scenario
The trap it hides
A
Two objects, edit one instance attribute
Does editing d1 touch d2?
B
Change a class attribute for everyone
Shared value updates for all
C
Shadowing: set instance attr with same name as class attr
Instance copy hides the class copy
D
Zero objects: touch only the class
No instance exists yet — what still works?
E
Identity vs equality of two look-alike objects
is False, but data equal
F
Degenerate: two names pointing at one object
Aliasing — one cookie, two labels
G
Limiting case: mutable class attribute shared by all
Silent cross-object corruption
H
Word problem: a real bank-account blueprint
Same rules, new costume
I
Exam twist: assign to class through an instance
Where does the write land?
The figure below is the mental model every example returns to.
Worked example Example 1 — Cell A: independent instance state
d1 = Dog( "Rex" , 3 )
d2 = Dog( "Fido" , 5 )
d1.age = 4
print (d1.age, d2.age)
Forecast: Guess the two printed numbers before reading on.
Dog("Rex", 3) builds a new object; __init__ writes name="Rex", age=3 into that object's own memory .
Why this step? Instance attributes are created by self.x = ..., which always targets the object being built — never the class.
Dog("Fido", 5) builds a second, unrelated object with its own age=5.
Why this step? Each call to the class produces a separate cookie from the cutter.
d1.age = 4 overwrites only d1's age cell.
Why this step? The dot on the left of = says "write into this object". d2 is a different cell, so it is untouched.
Verify: Output is 4 5. Sanity check: painting one house red never repaints the other. ✅
Worked example Example 2 — Cell B: class attribute changed for everyone
d1 = Dog( "Rex" , 3 )
d2 = Dog( "Fido" , 5 )
print (d1.species, d2.species)
Dog.species = "Lupus"
print (d1.species, d2.species)
Forecast: Four words will print. What are they?
d1.species finds no species on the object, so Python falls back to the class , reading "Canis". Same for d2.
Why this step? Attribute lookup checks the instance first, then the class. Instances borrow class attributes they don't own.
Dog.species = "Lupus" edits the single shared copy on the class.
Why this step? There is exactly one species cell (on the class); both objects point their lookup at it.
Both prints now report "Lupus".
Verify: Output Canis Canis then Lupus Lupus. One cutter changed → all cookies reflect it. ✅
Worked example Example 3 — Cell C: shadowing the class attribute
d1 = Dog( "Rex" , 3 )
d2 = Dog( "Fido" , 5 )
d1.species = "Wolf" # creates an INSTANCE attribute on d1
print (d1.species, d2.species, Dog.species)
Forecast: Three values. Which one differs?
d1.species = "Wolf" does not touch the class. Writing through an instance creates a new instance attribute on d1.
Why this step? Assignment with a dot always creates/updates a cell on the left-hand object . So d1 now has its own species cell that shadows (hides) the class one.
d1.species now finds its own cell first → "Wolf".
d2.species has no own copy → falls back to class → "Canis". Dog.species is still "Canis".
Verify: Output Wolf Canis Canis. The shared copy is unchanged; only d1 grew a private one. ✅
Worked example Example 4 — Cell D: zero objects, use only the class
print (Dog.species) # no object created yet
print (Dog. __init__ ) # the code exists on the class
# print(Dog.name) # would raise AttributeError
Forecast: Does Dog.species work with no dogs alive ? Does Dog.name?
Dog.species reads a class attribute — it lives on the class itself, so it is available even with zero instances.
Why this step? Defining the class already allocated its class attributes; instantiation is not required to read them.
Dog.__init__ is just the constructor code , stored on the class. Referencing it is fine.
Dog.name fails: name is an instance attribute, only born when some object runs self.name = .... With no object, no name exists.
Why this step? This is the degenerate boundary — it proves instance data has no home until instantiation.
Verify: Dog.species → "Canis". Dog.name → AttributeError. The drawing exists; no house's paint colour does. ✅
Worked example Example 5 — Cell E: identity vs equality
a = Dog( "Rex" , 3 )
b = Dog( "Rex" , 3 )
print (a is b)
print (a.name == b.name)
print ( type (a) is Dog and type (b) is Dog)
Forecast: Three booleans. Predict True/False for each.
a is b asks "same memory cell ?" Two separate Dog(...) calls → two cells → False.
Why this step? Identical data does not merge objects. See Identity vs Equality (is vs ==) .
a.name == b.name compares values : "Rex" == "Rex" → True.
type(a) is Dog and type(b) is Dog → both stamped from the same blueprint → True.
Verify: Output False, True, True. Same shape, separate cookies. ✅
Worked example Example 6 — Cell F: aliasing, one object with two names
a = Dog( "Rex" , 3 )
b = a # NO new object — same cell, second label
b.age = 10
print (a.age, b.age, a is b)
Forecast: Careful — does changing b change a?
b = a copies the reference , not the object. Both names point at one cell.
Why this step? = between two names never clones an object; it only adds a label. See Memory model and references .
b.age = 10 edits that one shared cell.
So a.age also reads 10, and a is b is True.
Verify: Output 10 10 True. Contrast with Cell A: there, two separate Dog(...) calls made two cells; here there is only one. ✅
Worked example Example 7 — Cell G: the mutable-class-attribute trap
class Kennel :
dogs = [] # MUTABLE class attribute — one shared list
def add (self, name):
self .dogs.append(name) # mutates the SHARED list, no new attr
k1 = Kennel()
k2 = Kennel()
k1.add( "Rex" )
print (k1.dogs, k2.dogs, k1.dogs is k2.dogs)
Forecast: After adding to k1 only, what does k2.dogs show?
dogs = [] is a class attribute : one list object, shared by every instance.
Why this step? Only assignment (self.dogs = ...) would create a per-object copy. .append(...) mutates in place — it does not assign, so the shared list is edited.
k1.add("Rex") looks up self.dogs → finds the class's list → appends to it.
k2.dogs is the same list, so it also shows ["Rex"]; k1.dogs is k2.dogs is True.
Verify: Output ['Rex'] ['Rex'] True. This is the limiting case where "shared class data" silently leaks across objects — the fix is to assign self.dogs = [] inside __init__. ✅
Worked example Example 8 — Cell H: real-world word problem (bank account)
A bank models accounts with a blueprint. The interest rate is the same for every account (a policy), but each account's balance is its own money . Alice deposits ₹100, Bob deposits ₹50, then the bank raises the rate. Model it and predict the final state.
class Account :
rate = 0.05 # CLASS: bank-wide policy
def __init__ (self, owner):
self .owner = owner
self .balance = 0 # INSTANCE: own money
def deposit (self, amt):
self .balance += amt
alice = Account( "Alice" ); alice.deposit( 100 )
bob = Account( "Bob" ); bob.deposit( 50 )
Account.rate = 0.07 # policy change for all
print (alice.balance, bob.balance, alice.rate, bob.rate)
Forecast: Four values.
balance is instance data → Alice's ₹100 and Bob's ₹50 stay independent (Cell A logic).
Why this step? Money must not leak between customers.
rate is a class attribute → changing Account.rate updates the value both accounts read (Cell B logic).
Why this step? A policy is shared by design.
Verify: Output 100 50 0.07 0.07. Own balance stays private; shared rate updates for both. ✅
Worked example Example 9 — Cell I: exam twist — assigning "through" an instance
alice = Account( "Alice" )
alice.rate = 0.09 # LOOKS like it changes the bank policy...
bob = Account( "Bob" )
print (alice.rate, bob.rate, Account.rate)
Forecast: The classic exam trap. Did Alice change the bank's rate?
alice.rate = 0.09 is an assignment through an instance . Just like Cell C, this creates a new instance attribute on alice, shadowing the class one.
Why this step? The rule is absolute: object.attr = value writes onto the object, never onto its class. Reading may fall back to the class; writing never does.
alice.rate now finds its own cell → 0.09.
bob.rate has no own copy → falls back → the class's 0.05. Account.rate is still 0.05.
Verify: Output 0.09 0.05 0.05. To change the policy for everyone you must write Account.rate = ... (Cell B), not alice.rate = .... ✅
Common mistake The single rule that resolves cells B, C, G, and I
Reading an attribute checks the instance first, then falls back to the class. Writing with object.attr = value always lands on the instance (creating a shadow). The only exceptions that touch shared state are: writing to Class.attr directly, or mutating (not reassigning) a shared mutable object like a list.
Recall Rebuild the matrix from memory
Edit d1.age, does d2 change? ::: No — instance data is per object (Cell A).
Dog.species = X, do all instances see X? ::: Yes — one shared class copy (Cell B).
d1.species = X, does the class change? ::: No — it creates a shadow on d1 only (Cell C, I).
a = Dog(); b = Dog(); a is b? ::: False — separate memory cells (Cell E).
b = a; b.age = 9; a.age? ::: 9 — same object, two labels (Cell F).
Shared list class attr, .append on one instance? ::: Every instance sees it — mutation hits the shared object (Cell G).
Constructors and __init__ — where instance attributes are born.
self and instance methods — how self targets the current object.
Class vs instance attributes — the shared-vs-own split behind every cell.
Encapsulation — bundling this state safely.
Identity vs Equality (is vs ==) — Cells E and F.
Memory model and references — aliasing in Cell F.