Visual walkthrough — Instance attributes vs class attributes
We only need three ideas, and we build each before using it:
- a box that holds name→value pairs (a namespace),
- two kinds of box (one on the class, one on each object),
- two different rules for reading vs writing into those boxes.
Then we push the rule to its limits: inheritance (more boxes upstairs), descriptors (some slots that do something when read), the hooks that wrap the whole search, deletion, and what happens when the search finds nothing at all.
Step 1 — What a namespace actually is (a labelled box)
WHAT: Draw one box. Put two slots in it: name → "Rex" and nothing else yet.
WHY: Before we can talk about "where an attribute lives", we need a concrete place for it to live. That place is a box. No box, no lookup — so we start here.
WHAT IT LOOKS LIKE: Look at the figure. The white rectangle is the box. The left column (cyan) is the label, the right column (amber) is the value. obj.__dict__ is literally "look inside this box and read the amber value next to a label."

obj::: the object whose box we are looking inside__dict__::: the name Python gives to that boxname::: a label (the left side of one slot)"Rex"::: the value stored under that label (the right side)
Step 2 — There are TWO boxes: one class box, many object boxes
WHAT: Draw the class box on top holding species → "Canis familiaris". Below it draw two empty object boxes a and b, each holding only its own name.
WHY: The whole distinction the parent note makes is geography: shared facts go in the top box, per-object facts go in the bottom boxes. Once we can see which box a value sits in, "shared vs owned" stops being a rule to memorise — it is just which rectangle the amber value is drawn in.
WHAT IT LOOKS LIKE: In the figure, species sits only in the top (class) box. The two object boxes have name but no species slot. That empty space is the entire reason lookup has to "search upward" — the value they want simply isn't in their own box.

class Dog:
species = "Canis familiaris" # goes in the CLASS box
def __init__(self, name):
self.name = name # goes in THIS object's box
a = Dog("Rex")
b = Dog("Fido")Dog.__dict__::: the top (class) box, holdsspeciesa.__dict__:::a's own box, holds onlyname → "Rex"b.__dict__:::b's own box, holds onlyname → "Fido"
Step 3 — The READ rule: search your own box first, then climb
WHAT: Trace a.species. Start arrow inside a's box → no species slot → arrow climbs up to the class box → found → return "Canis familiaris".
WHY these steps and not one lookup? Because sharing is exactly "the value isn't in my box, so I borrow the one upstairs." A single-box lookup could never let a thousand objects share one value. The upward search is the mechanism that turns the top box into a shared box.
WHY search the instance FIRST? So an object can override the shared value later (Step 5). Order matters: own-box-first means your personal note wins over the shared note. (There is one deliberate exception — a data descriptor on the class beats the instance box; we build that in Step 8.)
WHAT IT LOOKS LIKE: In the figure the cyan arrow starts inside a (empty for species), fails, and climbs the amber "fall-through" arrow to the class box where it lands on the value. b.species does the identical climb — both arrive at the same slot.

- step 1 ::: look in
a.__dict__— miss (nospecies) - step 2 ::: look in
Dog.__dict__— hit →"Canis familiaris" - the arrow stops the instant it hits, so step 3 (MRO) does not run here — that full climb up parent classes is Step 7.
Step 4 — The WRITE rule: always drop the value into your OWN box
WHAT: Run a.species = "Wolf". Watch a brand-new slot species → "Wolf" appear inside a's box. The class box does not move.
WHY is write asymmetric with read? Because assignment means "make this true for me". If writing climbed upward like reading, one object saying a.x = 5 would silently rewrite the shared value for everyone — a footgun. Python's designers made write always local, so self.x = ... can only ever affect the object doing the writing. This one asymmetry is the source of every "surprise" the parent note lists.
WHAT IT LOOKS LIKE: The figure shows the new amber slot materialising inside a, with a red "✗ never here" mark over the class box's species. Writing does not climb — it plants.

a.species = "Wolf"::: creates the keyspeciesinsidea.__dict__Dog.__dict__["species"]::: unchanged — still"Canis familiaris"
This is the mnemonic "Self for self, Class for class" made visible: to change the shared value you must write on the class name, Dog.species = .... See The self parameter for why self is the object's own box in disguise.
Step 5 — Shadowing: combine READ + WRITE and read again
WHAT: After a.species = "Wolf", re-run the three reads.
WHY: To prove the earlier steps predict the behaviour with no extra assumptions. We don't add a "shadowing rule" — we just replay read-order on the box we changed.
WHAT IT LOOKS LIKE: In the figure, a's arrow now stops immediately at its own amber "Wolf" slot (short cyan arrow, no climb). b's arrow still finds nothing locally and climbs to the class → "Canis familiaris". Two objects, two different answers, one shared class value untouched.

a.species = "Wolf"
print(a.species) # 'Wolf' → a stops at its own box
print(b.species) # 'Canis familiaris' → b climbs to class
print(Dog.species) # 'Canis familiaris' → class never changeda.species::: hitsa.__dict__at step 1 →"Wolf"b.species::: misses step 1, hits class at step 2 →"Canis familiaris"Dog.species::: reads the class box directly → still the original
Step 6 — Deletion: del a.species removes only YOUR slot, re-exposes the class
WHAT: Starting from a.species = "Wolf" (a shadowing slot in a), run del a.species. The species slot vanishes from a's box. Re-read a.species.
WHY show this? Because it is the only way to "undo" a shadow, and it proves shadowing was never overwriting: the shared value was patiently waiting one level up the whole time. Trying del a.species when a has no local slot raises AttributeError — you cannot delete something that isn't in your own box, and delete (like write) refuses to reach into the class box.
WHAT IT LOOKS LIKE: The figure shows the amber species slot being lifted out of a's box (dashed outline = removed), and the cyan read arrow, which used to stop early, now climbing again to the class value "Canis familiaris".

a.species = "Wolf"
del a.species # removes ONLY a's local slot
print(a.species) # 'Canis familiaris' (class value re-exposed)
del a.species # AttributeError: no local slot, del won't touch classdel a.species::: popsspeciesout ofa.__dict__- after delete :::
a's read climbs again → shared class value - second
del::: nothing local to remove →AttributeError
Step 7 — Inheritance: the climb continues up the MRO (parents and mixins)
WHAT: Build class Animal: with kingdom = "Animalia", a mixin class Barker: with sound = "woof", and class Dog(Barker, Animal):. Make a = Dog("Rex") and read a.kingdom, a.sound, a.name.
WHY this order? Dog(Barker, Animal) means "search Dog's own box, then Barker, then Animal, then object". Python computes this list once (Dog.__mro__) so multiple inheritance never gets ambiguous — every name has exactly one winner: the first box up the chain that holds it. See Inheritance and MRO for how the order is computed.
WHAT IT LOOKS LIKE: In the figure the boxes are stacked: a at the bottom, then Dog, Barker, Animal, object at the top. One long cyan arrow climbs the stack. a.name stops in a's own box; a.sound stops in Barker; a.kingdom climbs past Barker and Dog to Animal.

class Animal: kingdom = "Animalia"
class Barker: sound = "woof"
class Dog(Barker, Animal):
def __init__(self, name): self.name = name
a = Dog("Rex")
print(Dog.__mro__) # (Dog, Barker, Animal, object)
print(a.name) # 'Rex' → stops in a's own box
print(a.sound) # 'woof' → climbs to Barker (mixin)
print(a.kingdom) # 'Animalia' → climbs past to Animal (parent)Dog.__mro__::: the ordered list of boxes:Dog → Barker → Animal → objecta.sound::: first box holdingsoundisBarker→ stop therea.kingdom::: not inDogorBarker, found inAnimal
Step 8 — Descriptors: some class slots do something when read (and can outrank the instance)
WHAT: Give Dog a property called label (data descriptor) and a method bark (non-data descriptor). Show that a.bark returns a bound method (not the raw function), and that a.label runs the property's getter even after we try to plant a.__dict__["label"].
WHY this changes Step 3's "instance first" rule? Because a property must be able to guarantee it runs its getter/setter (validation, computed values) — if a stray instance slot could silently shadow it, the guarantee would break. So the read rule has one refinement: data descriptors on the class are checked before the instance box; everything else after.
WHAT IT LOOKS LIKE: In the figure, the class box holds two special (amber, gear-marked) slots. The label gear has a [get][set] tag and an arrow that jumps over a's box (data descriptor wins). The bark gear has only [get] and is drawn returning a "bound to a" tag; a same-named instance slot would still beat it.

class Dog:
def __init__(self, name): self.name = name
def bark(self): return "woof" # non-data descriptor (method)
@property
def label(self): # data descriptor (has get+set)
return "Dog:" + self.name
a = Dog("Rex")
print(a.bark) # <bound method Dog.bark of ...> (bound, not raw)
print(a.label) # 'Dog:Rex' (property getter ran)
a.__dict__["label"] = "HACK" # plant an instance slot with the SAME name
print(a.label) # STILL 'Dog:Rex' — data descriptor outranks instancebark::: non-data descriptor → returns a bound method, instance can still shadow itlabel(aproperty) ::: data descriptor → its getter wins even overa.__dict__["label"]- moral ::: "instance first" (Step 3) is true except for data descriptors
Step 9 — The hooks and the dead end: __getattribute__, __getattr__, AttributeError
WHAT: Trace one read to its very end. a.x → __getattribute__ runs the climb → nothing in a, nothing in Dog, nothing in object (top of MRO) → would raise AttributeError → but Dog.__getattr__ catches it and returns a default.
WHY show the dead end? Because the contract says never leave the reader at an undrawn scenario. Until now every read found something. This is the case where it does not — and the reader must see both outcomes: raw AttributeError, or the __getattr__ rescue.
WHY two different dunder names? __getattribute__ runs on every access (it is the search). __getattr__ runs on failed access only (the safety net). Mixing them up is the classic beginner error.
WHAT IT LOOKS LIKE: The figure shows the cyan climb going all the way up past object, hitting a red "dead end" wall. One branch (no __getattr__) throws AttributeError. The other branch (has __getattr__) diverts down an amber arrow into the fallback method, which returns a value.

class Dog:
def __init__(self, name): self.name = name
a = Dog("Rex")
# a.missing -> __getattribute__ climbs a, Dog, object, finds nothing
# -> AttributeError: 'Dog' object has no attribute 'missing'
class SafeDog:
def __init__(self, name): self.name = name
def __getattr__(self, key): # called ONLY when the normal climb fails
return f"<no {key}>"
s = SafeDog("Rex")
print(s.name) # 'Rex' → normal climb succeeds, __getattr__ NOT called
print(s.missing) # '<no missing>' → climb failed → __getattr__ rescue__getattribute__::: the method that runs every read (the whole search)- top of MRO :::
object— the climb always ends here - no match & no
__getattr__:::AttributeError(the true dead end) __getattr__::: fallback fired only on that failure
The one-picture summary
This final figure compresses all nine steps: the read enters through __getattribute__; a data descriptor on the class can win outright; otherwise the cyan READ arrow climbs your own box, then every class box up the MRO to object; a WRITE / DELETE arrow only ever touches your own box; and if the climb reaches the top with no hit, it either raises AttributeError or diverts to __getattr__.
Recall Feynman retelling — the whole walkthrough in plain words
Every object carries its own little box of labelled notes, and there's one big box on the class (and, if it has parents, a stack of big boxes climbing up to the universal box called object). When you read obj.x, a single doorman called __getattribute__ does the whole search: first it checks whether the class has a special locked slot (a property — a "data descriptor") that insists on running itself; if so, that wins even over your own note. Otherwise it checks your own box; if empty for that label, it climbs the stack of class boxes in a fixed order (the MRO) and stops at the first note it finds. A plain method found up there comes back "bound to you" so it knows who called it. When you write obj.x = ... you only ever scribble in your own box (shadowing the shared note), and del obj.x erases that same personal scribble, re-exposing the shared note underneath — but neither write nor delete can touch the shared boxes. Finally, if the doorman climbs all the way to object and still finds nothing, it either shouts AttributeError or, if you gave the class a safety net called __getattr__, quietly calls that instead. Two arrows and one doorman explain everything: read climbs (data descriptors first), write and delete plant only in your own box.
Connections
- Instance attributes vs class attributes (parent)
- Namespaces and scope — the
__dict__boxes are namespaces - The self parameter —
selfis the object's own box - __init__ constructor — where per-object writes belong
- Classes and objects
- Inheritance and MRO — the climb up the MRO (Steps 7–9)
- Mutable vs immutable objects — why lists share, ints don't