This page hunts down every way self can show up in real code and drags each one into the light. If you have not yet seen what self is , read the parent first: self — the parent note and the machinery in Bound and Unbound Methods .
Intuition The single fact we keep re-using
The dot fills the first slot. obj.method(args) becomes Class.method(obj, args). Every example below is just this one rule under different lighting.
Before any code, let's list every case class a self question can belong to. Each worked example below is tagged with the cell it fills.
#
Cell (the scenario)
What makes it tricky
A
Two instances, one body
proving states stay separate
B
Manual pass through the class
Class.m(obj) vs obj.m()
C
Method returns self
chaining / fluent APIs
D
self reassigned inside a method
rebinding the local name does nothing outside
E
self renamed (this, me)
proving it's a convention
F
Degenerate: no self in signature
the TypeError and its off-by-one arg count
G
Degenerate: passing self twice
obj.m(obj) too-many-args
H
Bound-method object stored/passed
alice.deposit as a first-class value
I
Word problem (real world)
translate English → which self
J
Exam twist : classmethod / staticmethod
when there is no self at all
We now cover A → J with 9 examples. Related landmarks: Classes and Instances , __init__ — the constructor , Instance vs Class Attributes , Descriptor Protocol , classmethod and staticmethod .
The figure above is the whole page in one picture: the red arrow is the dot handing the object into the first slot. Keep it in your eye.
Worked example Ex 1 — Cell A: two instances, one method body
class Counter :
def __init__ (self):
self .n = 0
def inc (self):
self .n += 1
a = Counter(); b = Counter()
a.inc(); a.inc(); b.inc()
print (a.n, b.n)
Forecast: what prints — guess before reading on.
a.inc() → Counter.inc(a). Why this step? The dot fills slot 1 with a, so self is a; self.n += 1 touches a.n.
Do it again: a.n goes 0 → 1 → 2. Why this step? Same body, same self = a, so the same attribute climbs.
b.inc() → Counter.inc(b), self = b, so b.n goes 0 → 1. Why this step? Different object in the slot ⇒ different attribute.
Verify: a.n == 2, b.n == 1. Output 2 1. States never crossed because self pointed at different objects.
Worked example Ex 2 — Cell B: manual pass through the class
a = Counter()
Counter.inc(a)
Counter.inc(a)
print (a.n)
Forecast: does this even work without a dot?
Counter.inc is the raw function (unbound), so it does not auto-fill slot 1. Why this step? No instance on the left of a dot means nothing is injected — see Bound and Unbound Methods .
So we hand a in ourselves: Counter.inc(a) sets self = a. Why this step? We are manually doing what the dot normally automates.
Twice ⇒ a.n = 0 → 1 → 2. Why this step? Identical to a.inc(); a.inc().
Verify: a.n == 2. Proves a.inc() ≡ Counter.inc(a).
Worked example Ex 3 — Cell C: returning
self for chaining
class Box :
def __init__ (self): self .items = []
def add (self, x):
self .items.append(x)
return self
b = Box().add( 1 ).add( 2 ).add( 3 )
print (b.items)
Forecast: what is b, and what's inside it?
Box() builds one object; call it B. Why this step? __init__ runs, gives B.items = [].
.add(1) runs B.items.append(1) then return self → returns B itself. Why this step? Returning self means the same object flows to the next dot.
.add(2) and .add(3) operate on that same B. Why this step? Each returned self is B, so the chain never switches objects.
Verify: b is B and b.items == [1, 2, 3]. Chaining works precisely because self is handed back.
Worked example Ex 4 — Cell D: reassigning
self inside a method (does nothing outside)
class C :
def __init__ (self): self .x = 1
def sneaky (self):
self = C() # rebind the LOCAL name
self .x = 999
return self .x
c = C()
print (c.sneaky(), c.x)
Forecast: does c.x become 999?
c.sneaky() → C.sneaky(c), so the local self starts as c. Why this step? The dot filled the slot with c.
self = C() rebinds the local variable self to a brand-new object. Why this step? In Python assigning to a name only changes what that local name points to — the outside c is untouched.
self.x = 999 sets x on the new object; return self.x → 999. Why this step? We are reading the new object, not c.
Back outside, c.x is still 1. Why this step? Nothing ever wrote to c.
Verify: returns 999, but c.x == 1. Rebinding self is harmless to the caller — a classic gotcha.
Worked example Ex 5 — Cell E: renaming
self to prove it's a convention
class Dog :
def __init__ (this, name): # 'this' not 'self'
this.name = name
def bark (me): # 'me' not 'self'
return me.name + " says woof"
print (Dog( "Rex" ).bark())
Forecast: legal Python, or syntax error?
Dog("Rex") → Dog.__init__(newobj, "Rex"). Why this step? The dot still fills slot 1; the name of that slot (this) is irrelevant to Python.
.bark() → Dog.bark(theDog), so me = theDog. Why this step? Same rule; me just labels the injected instance.
me.name → "Rex", concatenate → "Rex says woof". Why this step? Attribute access works through whatever name holds the instance.
Verify: result is "Rex says woof". self is [!definition] never a keyword — pure convention.
Worked example Ex 6 — Cell F: forgetting
self (degenerate signature)
class Bell :
def ring (): # no self!
return "ding"
Bell().ring()
Forecast: how many arguments does Python think arrived?
Bell().ring() → Bell.ring(instance). Why this step? The dot always injects the instance — you cannot opt out by omitting self.
ring() declares 0 parameters, but 1 argument (the instance) arrived. Why this step? Mismatch = TypeError.
Message: ring() takes 0 positional arguments but 1 was given. Why this step? The "1" is the instance you did not name.
Verify: raising this TypeError is the checkable fact. Fix: def ring(self):.
Worked example Ex 7 — Cell G: passing
self twice (degenerate call)
class Bank :
def __init__ (self): self .bal = 0
def deposit (self, amt):
self .bal += amt
return self .bal
alice = Bank()
alice.deposit(alice, 50 )
Forecast: three arguments or two reach deposit?
alice.deposit(...) already injects alice into slot 1 via the dot. Why this step? The dot fills the slot before your written args.
Your written args are (alice, 50) → total arguments become (alice, alice, 50) = 3 . Why this step? You duplicated the instance.
deposit accepts only (self, amt) = 2 → TypeError: too many arguments. Why this step? 3 into 2 does not fit.
Verify: TypeError again. Fix: alice.deposit(50).
Worked example Ex 8 — Cell H: a bound method as a first-class value
class Bank :
def __init__ (self, bal): self .bal = bal
def deposit (self, amt):
self .bal += amt
return self .bal
alice = Bank( 100 )
f = alice.deposit # grab the bound method, don't call yet
print (f is alice.deposit) # each access makes a fresh bound object
print (f. __self__ is alice)
print (f( 25 ))
Forecast: is f is alice.deposit True or False? And what does f(25) print?
alice.deposit triggers the descriptor protocol , which builds a new bound-method object each time. Why this step? See Descriptor Protocol — the function's __get__ packages (function, alice).
Because each access builds a fresh package, f is alice.deposit is False . Why this step? Two separate objects, same behaviour.
f.__self__ is exactly alice. Why this step? The bound object stores the instance it will inject.
f(25) → Bank.deposit(alice, 25) → 100 + 25 = 125. Why this step? Calling injects __self__ into slot 1.
Verify: f is alice.deposit → False; f.__self__ is alice → True; f(25) == 125.
Worked example Ex 9 — Cell I + Cell J: word problem, then the exam twist
Statement (Cell I, real world): A vending machine tracks its own coin count. Two machines sit side by side. A customer drops a coin in machine left. Then the operator asks the class itself how many machines exist — that count is not tied to any single machine.
class Vending :
count = 0 # CLASS attribute (shared)
def __init__ (self):
self .coins = 0 # INSTANCE attribute (per machine)
Vending.count += 1 # bump the shared counter
def insert (self, c): # needs self: WHICH machine
self .coins += c
return self .coins
@ classmethod
def how_many (cls): # Cell J: NO self, gets cls
return cls .count
@ staticmethod
def coin_ok (c): # Cell J: NO self, NO cls
return c in ( 1 , 2 , 5 )
left = Vending(); right = Vending()
left.insert( 5 ); left.insert( 2 )
print (left.coins, right.coins)
print (Vending.how_many())
print (Vending.coin_ok( 2 ), Vending.coin_ok( 3 ))
Forecast: four outputs — write your guesses now.
left.insert(5) → Vending.insert(left, 5), self = left, left.coins = 0→5. Why this step? The dot chose which machine via self. See Instance vs Class Attributes .
left.insert(2) → left.coins = 5→7; right.coins never touched, stays 0. Why this step? self pointed at left only.
how_many is a classmethod : no self, first slot is cls = Vending. It reads the shared count, incremented twice in __init__ → 2. Why this step? A count of machines belongs to the class, not one instance — so no self. See classmethod and staticmethod .
coin_ok is a staticmethod : neither self nor cls is injected — it's a plain helper. coin_ok(2) → True, coin_ok(3) → False. Why this step? No per-object data needed, so no first slot at all.
Verify: left.coins == 7, right.coins == 0, how_many() == 2, coin_ok(2) is True, coin_ok(3) is False. This is the full contrast: self (which instance) vs cls (which class) vs nothing (pure function).
Recall Does rebinding
self = something inside a method change the caller's object?
No. It only re-points the local name self; the caller's object is untouched (Ex 4).
Recall Is
f is alice.deposit True?
No — each attribute access builds a fresh bound-method object, so it's False (Ex 8).
Recall In a
classmethod, what fills the first slot instead of the instance?
cls, the class itself — not any instance (Ex 9).
Recall Why does
alice.deposit(alice, 50) fail?
The dot already injected alice, so you passed it twice → too many arguments (Ex 7).
Mnemonic One line for the whole matrix
"Dot fills the slot; self, cls, or nothing sits in it." Instance method → self. classmethod → cls. staticmethod → nobody.
separate states, chaining, bound methods