2.1.5 · D4OOP Fundamentals

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

1,983 words9 min readBack to topic

Parent topic: `self` — what it is and how Python passes it · chapter OOP Fundamentals.


Level 1 — Recognition

Can you see the hidden rewrite and name what self is?

Recall Solution 1.1

The dot fills the first slot with rex: So inside the method: self ← rex and times ← 3. What we did: applied the single rewrite rule. Why: it removes the "magic" — there is no hidden global, only argument binding.

Recall Solution 1.2

False. self is a naming convention, not a keyword. def bark(this, times): this.x = 1 works identically — this receives the instance. We use self only so other programmers instantly recognise it.


Level 2 — Application

Predict the output by doing the rewrite yourself.

Recall Solution 2.1

Rewrite each call: a.inc(2)Counter.inc(a, 2) touches a.n; b.inc(10)Counter.inc(b, 10) touches b.n. The bodies are the same code, but self differs, so the states never mix.

  • a.n:
  • b.n:

Output: 5 10. See Instance vs Class Attributes for why each .n lives on its own object.

Recall Solution 2.2

Here we pass self = a by hand — no dot, no auto-injection. a.n: . Output: 10. This is the proof that a.inc(x) and Counter.inc(a, x) are the same operation. See Bound and Unbound Methods.


Level 3 — Analysis

Explain the failure, not just spot it.

Recall Solution 3.1

C().greet() rewrites to C.greet(C()) — the dot still fills the first slot with the new instance. So greet receives 1 argument (the instance), but its signature accepts 0. That mismatch is the error. Fix: def greet(self): — add the slot the dot needs to fill.

Recall Solution 3.2

Bare balance is looked up as a local variable of deposit, not as instance data. Instance attributes live on the object and are reached only through self. — see __init__ — the constructor where self.balance was created. Writing balance += amount tries to read a local that was never assigned → error. Fix:

def deposit(self, amount):
    self.balance += amount
    return self.balance

With bal=100, amount=50 this returns 150.


Level 4 — Synthesis

Combine the rule with return-values and chaining.

Recall Solution 4.1

Each add mutates self.items then hands the same object back. So .add(20) runs on the object .add(10) returned — which is the very same Box.

  • after add(10): [10]
  • after add(20): [10, 20]
  • after add(30): [10, 20, 30]

Output: [10, 20, 30]. Why return self: without it, add returns None, and the next .add would raise AttributeError: 'NoneType' object has no attribute 'add'.

Recall Solution 4.2

self.n is per-instance (each object has its own); Tally.total is a class attribute, shared by all.

  • t.n:
  • u.n:
  • Tally.total: (every hit bumps it)

Output: 2 1 3. This is the boundary between self (instance) and class-level state — see classmethod and staticmethod for the cls-based version.


Level 5 — Mastery

Build the self-passing machinery from raw parts.

Recall Solution 5.1
def make_account(owner, balance):
    return {"owner": owner, "balance": balance}
 
def deposit(self, amount):          # 'self' is just our first param
    self["balance"] += amount
    return self["balance"]
 
alice = make_account("Alice", 100)
print(deposit(alice, 50))           # 150 — WE pass the object by hand

Result: 150. In a real class, alice.deposit(50) does the deposit(alice, 50) part automatically — that "handing over alice" is self. Nothing else is added.

Recall Solution 5.2
def bind(f, obj):
    def bound(*args, **kwargs):
        return f(obj, *args, **kwargs)   # inject obj as self
    return bound
 
class Dog:
    def bark(self, times): return "woof " * times
 
rex = Dog()
my_bound = bind(Dog.bark, rex)
print(my_bound(2))          # 'woof woof '
print(rex.bark(2))          # 'woof woof '  — same thing

Both print 'woof woof '. Python's real machinery is the Descriptor Protocol: accessing rex.bark calls bark.__get__(rex, Dog), which returns exactly this kind of function + instance package (a bound method). Our bind is a hand-rolled version of that.

Recall Solution 5.3
  • rex.bark.__self__ is the rex instance — the object stored in the bound method.
  • rex.bark.__func__ is the plain underlying function Dog.bark (the one whose first param is self). Calling rex.bark(2) is literally rex.bark.__func__(rex.bark.__self__, 2).

Recall summary

Recall Q: What is the single rewrite rule tested by every exercise here?

obj.method(args)Class.method(obj, args) — the dot fills the first slot with obj (that's self).

Recall Q: Why does

def greet(): fail when called on an instance? The dot still passes the instance, so 1 argument arrives at a 0-parameter function.

Recall Q: What breaks method chaining?

Forgetting return self; the method returns None and the next call hits NoneType.


Connections

Difficulty Map

L1 Recognition rewrite the dot

L2 Application predict output

L3 Analysis explain the error

L4 Synthesis chaining and cls

L5 Mastery build self by hand

Dot fills the slot