Exercises — `self` — what it is and how Python passes it
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.balanceWith 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: (everyhitbumps 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 handResult: 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 thingBoth 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 therexinstance — the object stored in the bound method.rex.bark.__func__is the plain underlying functionDog.bark(the one whose first param isself). Callingrex.bark(2)is literallyrex.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
- OOP Fundamentals
- Classes and Instances
- __init__ — the constructor
- Instance vs Class Attributes
- Bound and Unbound Methods
- Descriptor Protocol
- classmethod and staticmethod