2.1.3 · D4OOP Fundamentals
Exercises — Instance attributes vs class attributes
Keep this picture beside every exercise:

The blue box is one instance's own notebook (a.__dict__). The orange box is the shared whiteboard (Dog.__dict__). The green arrow is a READ climbing up; the red arrow is a WRITE that always drops into the blue box.
Level 1 · Recognition
L1.1 — Where does it live?
Recall Solution
nameis created by the assignmentself.name = name→ it lives in the instance namespacec.__dict__.legsis written in the class body (underclass Cat:, not inside a method) → it lives in the class namespaceCat.__dict__. There is one copy shared by every cat.- So
c.__dict__ == {'name': 'Milo'}. It does not containlegs. - When you read
c.legs, step 1 (instance) misses, step 2 (class) hits → returns4.
L1.2 — Two objects, one whiteboard
Recall Solution
- Neither
anorbhaslegsin its own__dict__, so both reads fall through to the class → both return4. Cat.legsis also4.- Since all three reads resolve to the one class-level object,
a.legs is b.legs is Cat.legsisTrue. (4is a single shared integer here.)
Level 2 · Application
L2.1 — Shadowing a class attribute
Recall Solution
a.species = "Wolf"is a WRITE → drops a new key intoa.__dict__(the blue box in the figure). The class is untouched.a.species→ instance search hits first →"Wolf".b.species→b.__dict__has nospecies→ falls through to class →"Canis familiaris".Dog.species→ still the original"Canis familiaris"."species" in a.__dict__→True(the write created it there); the same check onbwould beFalse.
L2.2 — Changing it for everyone
Recall Solution
Dog.species = "Doggo"is a WRITE on the class name → it changes the single shared copy on the whiteboard.a.species→astill has its own"Wolf"ina.__dict__(step 1 wins) →"Wolf".ais deaf to the class change because it shadows.b.species→ no instance copy → falls through to the updated class →"Doggo".
L2.3 — Un-shadow
Recall Solution
del a.speciesremoves the keyspeciesfroma.__dict__(it only ever deletes from the instance).- Now
ahas no ownspecies, so a read falls through to the class →"Doggo". - The shadow is gone;
are-joins everyone else.
Level 3 · Analysis
L3.1 — The mutable-list trap
Recall Solution
self.tricks.append(t)has no=assignment. It is a READ that finds the shared class list (whiteboard), then mutates that one object.- Both appends land in the same list: after both calls the list is
["roll", "spin"]. - So
a.tricks,b.tricks, andDog.tricksall return the same["roll", "spin"]. a.tricks is b.tricks→True(literally one object).
L3.2 — When does a per-instance copy actually appear?
Recall Solution
a.tricks = ["roll"]is a WRITE (assignment) → creates a fresh list ina.__dict__.anow shadows.b.tricks.append("spin")is READ + mutate →bhas no owntricks, so it finds the class list and mutates it.- Result:
a.tricks == ["roll"](its own),b.tricks == ["spin"]andDog.tricks == ["spin"](same shared list). - Lesson: whether you share depends only on whether you used
=(assignment ⇒ own copy) or a mutation (⇒ shared).
L3.3 — Counter across instances
Recall Solution
Counter.count += 1reassigns on the class each time → the single shared copy goes1, 2, 3. SoCounter.count == 3.x.countreads:x.__dict__has nocount→ falls through to class →3. Same fory,z."count" in x.__dict__→False— no instance ever wrote its owncount.- Why
self.count += 1breaks it:self.count += 1expands toself.count = self.count + 1. The right side reads the class value (say0), and the left side is a WRITE → it stashes1in that instance's__dict__. Each object would end at its own1, and the class stays0. The counter would never accumulate.
Level 4 · Synthesis
L4.1 — Fix the bug
Recall Solution
(1) Bug:
p = Account("Pat"); q = Account("Quinn")
p.spend(10); q.spend(20)
print(p.log) # [10, 20] ← Quinn's spend leaked in
print(q.log) # [10, 20]self.log.append is read+mutate of the one class list.
(2) Fix — assign a fresh list per instance:
class Account:
def __init__(self, owner):
self.owner = owner
self.log = [] # assignment ⇒ own list per object
def spend(self, amt):
self.log.append(amt)Now:
p = Account("Pat"); q = Account("Quinn")
p.spend(10); q.spend(20)
print(p.log) # [10]
print(q.log) # [20](3) In spirit the change is: move the list from the class body to an assignment inside __init__ — introduce an = per instance so each object forks its own copy.
L4.2 — Design decision
Recall Solution
- (a)
GRAVITATIONAL_CONSTANT→ class attribute. Truly shared, immutable float. One copy on the class; reads fall through. No mutation risk. - (b)
mass→ instance attribute (self.mass = ...in__init__). Differs per object; a class attribute would make all planets collide on one value. - (c)
moons→ instance attribute (self.moons = []in__init__). You will mutate it (.append), so it must be a fresh per-object list — otherwise the L3.1 trap bites. - (d)
registry→ class attribute, and you mutate it deliberately:Planet.registry.append(self)(orself.__class__.registry.append(self)) inside__init__. Here sharing is exactly what you want — one global list of all planets. This is the rare legitimate mutable class attribute.
Level 5 · Mastery
L5.1 — Predict the whole trace
Recall Solution
b.kind = "custom"is a WRITE on the instance →b.__dict__['kind'] = "custom".b.kind→ instance hit →"custom".c.kind→c.__dict__empty →Child.__dict__haskind = "child"→"child"(never reachesBase).Base.kind→ unchanged →"base".tags: neitherBasenorChilddefines its owntagsexcept the one inBase.c.tagsreads:c(miss) →Child(miss) →Base→ the shared list.c.tags.append("x")mutates that list.b.tags→b(miss) →Base→ same list →["x"].c.tags→["x"].Base.tags→["x"].b.tags is c.tags→True: both resolve, via the MRO, to the singleBase.tagslist. Inheritance makes the sharing reach even further.
L5.2 — The is autopsy
Recall Solution
- (1)
True— both read through to the one class list[1, 2]. - (2)
a.data + [3]builds a brand-new list[1, 2, 3](the+operator creates a new object, it does not mutate). Thena.data = ...is a WRITE → stores that new list ina.__dict__.anow shadows. - (3)
a.data→ instance hit →[1, 2, 3]. - (4)
b.data→ no instance copy → class list, untouched →[1, 2]. - (5)
a.data is T.data→False:a's is its own new list; the class still has the original. - Why
+≠.append:list + listreturns a new list (no in-place change), and the surrounding=forks a private copy — safe..appendmutates in place with no assignment — it edits the shared object and leaks. See Mutable vs immutable objects for the general rule: rebinding is private, mutating is shared.
Connections
- 2.1.03 Instance attributes vs class attributes · the parent
- OOP Fundamentals
- Classes and objects
- The self parameter
- __init__ constructor
- Inheritance and MRO
- Namespaces and scope
- Mutable vs immutable objects