2.1.3 · D4OOP Fundamentals

Exercises — Instance attributes vs class attributes

2,593 words12 min readBack to topic

Keep this picture beside every exercise:

Figure — Instance attributes vs class attributes

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
  • name is created by the assignment self.name = name → it lives in the instance namespace c.__dict__.
  • legs is written in the class body (under class Cat:, not inside a method) → it lives in the class namespace Cat.__dict__. There is one copy shared by every cat.
  • So c.__dict__ == {'name': 'Milo'}. It does not contain legs.
  • When you read c.legs, step 1 (instance) misses, step 2 (class) hits → returns 4.

L1.2 — Two objects, one whiteboard

Recall Solution
  • Neither a nor b has legs in its own __dict__, so both reads fall through to the class → both return 4.
  • Cat.legs is also 4.
  • Since all three reads resolve to the one class-level object, a.legs is b.legs is Cat.legs is True. (4 is 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 into a.__dict__ (the blue box in the figure). The class is untouched.
  • a.species → instance search hits first → "Wolf".
  • b.speciesb.__dict__ has no species → 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 on b would be False.

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.speciesa still has its own "Wolf" in a.__dict__ (step 1 wins) → "Wolf". a is 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.species removes the key species from a.__dict__ (it only ever deletes from the instance).
  • Now a has no own species, so a read falls through to the class → "Doggo".
  • The shadow is gone; a re-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, and Dog.tricks all return the same ["roll", "spin"].
  • a.tricks is b.tricksTrue (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 in a.__dict__. a now shadows.
  • b.tricks.append("spin") is READ + mutate → b has no own tricks, so it finds the class list and mutates it.
  • Result: a.tricks == ["roll"] (its own), b.tricks == ["spin"] and Dog.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 += 1 reassigns on the class each time → the single shared copy goes 1, 2, 3. So Counter.count == 3.
  • x.count reads: x.__dict__ has no count → falls through to class → 3. Same for y, z.
  • "count" in x.__dict__False — no instance ever wrote its own count.
  • Why self.count += 1 breaks it: self.count += 1 expands to self.count = self.count + 1. The right side reads the class value (say 0), and the left side is a WRITE → it stashes 1 in that instance's __dict__. Each object would end at its own 1, and the class stays 0. 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) (or self.__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.kindc.__dict__ empty → Child.__dict__ has kind = "child""child" (never reaches Base).
  • Base.kind → unchanged → "base".
  • tags: neither Base nor Child defines its own tags except the one in Base. c.tags reads: c (miss) → Child (miss) → Base → the shared list. c.tags.append("x") mutates that list.
  • b.tagsb (miss) → Basesame list → ["x"].
  • c.tags["x"]. Base.tags["x"].
  • b.tags is c.tagsTrue: both resolve, via the MRO, to the single Base.tags list. 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). Then a.data = ... is a WRITE → stores that new list in a.__dict__. a now 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.dataFalse: a's is its own new list; the class still has the original.
  • Why +.append: list + list returns a new list (no in-place change), and the surrounding = forks a private copy — safe. .append mutates 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