2.1.5 · D2OOP Fundamentals

Visual walkthrough — `self` — what it is and how Python passes it

1,825 words8 min readBack to topic

We will link back to the main `self` note and lean on Classes and Instances and Bound and Unbound Methods as we go.


Step 1 — An object is just a labelled box of data

WHAT. Before methods, before self, there is only data that belongs together. An account has an owner and a balance. Picture a box with two labelled slots inside it.

WHY. You cannot understand "the method needs to know which object" until you can see that there are several boxes, each with its own values. The whole reason self exists is to point at one specific box.

PICTURE. Two boxes, alice and bob. Same slot-names (owner, balance), different contents. Nothing is shared between them yet.

Figure — `self` — what it is and how Python passes it

Step 2 — A method is a plain function whose first slot waits for a box

WHAT. Now we add behaviour. A "deposit" is a function that takes a box and an amount, and changes that box's balance.

WHY. We deliberately write it as an ordinary function first, with the box as an explicit argument, so you can see there is no magic — just a normal parameter. We name that first parameter self only because that is the convention; box would work identically.

PICTURE. A function deposit drawn as a machine with two input slots. Slot 1 is labelled self and is shaped to receive a box. Slot 2 is labelled amount.

Figure — `self` — what it is and how Python passes it

Step 3 — Call it manually: we hand the box in ourselves

WHAT. Let's run the function the honest way — passing the box by hand: deposit(alice, 50).

WHY. This is the ground truth. Everything Python does automatically later is just this manual call in disguise. If you understand this line, you understand self.

PICTURE. The alice box slides into slot 1; the number 50 slides into slot 2. Inside the machine, self now is alice, so self.balance += amount writes back into alice's balance slot — 100 → 150.

Figure — `self` — what it is and how Python passes it
def deposit(self, amount):     # 'self' is just parameter #1
    self.balance += amount     # write into THIS box's balance
    return self.balance
 
deposit(alice, 50)   # WE hand 'alice' to slot 1  ->  150

Step 4 — The dot is a shortcut for "put the left box in slot 1"

WHAT. Python lets you write alice.deposit(50) instead of deposit(alice, 50). The two are equivalent.

WHY. Writing the box twice (deposit(alice, ...) where alice is obviously "the thing doing the depositing") is clumsy. The dot exists precisely so the box on its left automatically fills slot 1. This is the "syntactic sugar" the parent note mentioned — now you can see what the sugar sweetens.

PICTURE. The expression alice.deposit(50). A curved arrow lifts alice (the thing left of the dot) and drops it into slot 1, while 50 goes to slot 2.

Figure — `self` — what it is and how Python passes it

Step 5 — How the dot actually does it: the bound method

WHAT. When you type alice.deposit (without calling it yet), Python does not hand you the raw function. It hands you a small wrapper — a bound method — that has already remembered alice.

WHY. Python needs somewhere to store "which box goes in slot 1" between the moment you look up alice.deposit and the moment you call it with (50). That storage is the bound method. This is the machinery behind Bound and Unbound Methods and the Descriptor Protocol.

PICTURE. alice.deposit produces a little package holding two things: __func__ (the raw deposit function) and __self__ (the alice box). When you finally call (50), the package pours its stored alice into slot 1 and your 50 into slot 2.

Figure — `self` — what it is and how Python passes it
bm = alice.deposit
bm.__func__   # the plain deposit function (slot 1 still empty)
bm.__self__   # the alice box  (what will fill slot 1)
bm(50)        # == deposit(alice, 50)

Step 6 — Same body, different self: why states stay separate

WHAT. Run the method on both boxes: alice.deposit(50) then bob.deposit(20).

WHY. This is the payoff of the whole design. One function body serves every instance, because each call binds a different box to slot 1. Change the box, change whose data you touch — the code never changes.

PICTURE. The single deposit machine used twice: first with alice in slot 1 (only alice's balance moves), then with bob in slot 1 (only bob's balance moves). Two independent results from one machine.

Figure — `self` — what it is and how Python passes it
Recall Why don't

alice and bob interfere? Because self.balance += amount writes into whatever box is in slot 1. Different call ⇒ different box ⇒ different balance slot. The shared code touches unshared data.


Step 7 — The degenerate cases (where the slot goes wrong)

WHAT. Now the edge cases — every way the slot can misbehave. Each is a direct consequence of "the dot fills slot 1".

WHY. If you understand the slot, you can predict every one of these errors instead of memorising them. That is the test of real understanding.

PICTURE. Three broken machines side by side, each showing exactly which slot mismatched.

Figure — `self` — what it is and how Python passes it
  • Case A — no slot 1 in the signature. def greet(): has zero slots. But the dot still delivers alice into slot 1. One box arrives at a zero-slot machine → TypeError: greet() takes 0 positional arguments but 1 was given. Fix: def greet(self):.
  • Case B — you fill slot 1 yourself and use the dot. alice.deposit(alice, 50): the dot already put alice in slot 1, then you shove alice in again plus 50 → three things for two slots → TypeError: too many arguments. Fix: alice.deposit(50).
  • Case C — bare name, no self.. Inside the body, balance += amount reads balance as a brand-new local name (not in any box) → NameError. The box is in slot 1 under the name self; you must go through it: self.balance. Fix: self.balance += amount.

The one-picture summary

Here is the entire derivation compressed: a box on the left of a dot, lifted into slot 1 of a shared function, producing the equivalent explicit call — with the bound-method package shown as the thing that remembers the box.

Figure — `self` — what it is and how Python passes it
Recall Feynman retelling — the whole walkthrough in plain words

Think of a kitchen with one printed recipe (the function) that says "add sugar to your bowl". Many cooks each own a bowl (the instances alice, bob). The recipe has a first blank at the top: "your bowl goes here" — that blank is self. When you tell a cook "run the recipe!", you hand them their own bowl first, then the ingredients. That handing-over is what the dot does: alice.deposit(50) means "give the recipe alice's bowl, then 50". Behind the scenes, the moment you say "alice's recipe" (alice.deposit), the kitchen staples a sticky note with alice's bowl onto a copy of the recipe — that stapled pair is the bound method (__func__ = recipe, __self__ = bowl). So self was never magic: it is just "the bowl in the top blank", and the only clever bit is that the dot fills that blank for you. Every error (forgetting the blank, filling it twice, forgetting to say "self." before "bowl") is just the blank being counted or routed wrong.


Connections

Concept Map

left of dot

slot 1 waits

produces

call injects box

so

different box

slot miscount

Instance a labelled box

The dot fills slot 1

Function first param self

Bound method func plus instance

Class method instance args

self equals the box

Separate states

TypeError or NameError