PYTHON / OBJECT-ORIENTED PYTHON
Instance methods and self
Write instance methods that read and mutate per-object state through self, and explain why obj.m(x) is exactly Class.m(obj, x).
What you will learn
- Explain why c.increment(5) is the same call as Counter.increment(c, 5)
- Read and mutate per-instance state through self instead of bare names
- Inspect a bound method with __self__ and __func__ and pass it as a callable
- Recognise the TypeError produced by omitting self from a method signature
Understanding Instance methods and self
An instance method is nothing more exotic than a function defined in a class body. The class stores it; the function body has no built-in awareness of any object. What makes it a method is how you reach it: when you write c.increment(), Python looks for increment in the instance's own dictionary, fails, finds the function on the class, and because functions implement the descriptor protocol, hands you back a bound method object that has already remembered c. Calling that bound method inserts c as the first argument, which is why the parameter list needs a slot for it.
That first parameter, conventionally named self, is your only handle on the object being operated on. Every piece of per-instance data has to be reached as self.value, self.name, and so on; writing value = value + 1 inside a method just creates a local variable that vanishes when the method returns, and reading a bare value looks in the local and global scopes, never on the object. The same rule applies to calling siblings: self.area() dispatches through the instance, so if a subclass overrides area, the override runs.
Because binding happens at attribute-access time, a bound method is a first-class value you can store, put in a list, or hand to a callback. It carries the instance in __self__ and the original plain function in __func__, so a.greet and g.greet are two different objects wrapping the same function. The explicit self also means the unbound route always stays open: Counter.increment is just a function, and you may call it with the instance passed by hand, which is exactly what Python does for you behind the dot.
class Counter:
def __init__(self, start=0):
self.value = start
def increment(self, by=1):
self.value += by
return self.value
def describe(self):
return f"Counter at {self.value}"
c = Counter(10)
print(c.increment())
print(c.increment(5))
print(c.describe())
# The bound call and the explicit call are the same operation
print(Counter.increment(c, 100))
print(c.value)
print(c.increment.__self__ is c)
print(type(Counter.increment).__name__, type(c.increment).__name__)Looking a function up through an instance produces a bound method, which is why the instance arrives in the method's first parameter, self.
Worked examples
Mutating state and chaining through self
Methods change the object they were called on, and returning self lets calls chain.
class Rectangle:
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
def scale(self, factor):
self.w *= factor
self.h *= factor
return self
def summary(self):
return f"{self.w}x{self.h} area={self.area()}"
r = Rectangle(2, 3)
print(r.summary())
print(r.scale(2).scale(3).summary())
print(r.w, r.h)Example explained
Line 1self.w *= factor rebinds an attribute on the object, so the change outlives the call.
Line 2return self hands back the same object, which is why .scale(2).scale(3) works on one rectangle.
Line 3summary calls self.area() rather than recomputing, so any subclass that redefines area is honoured.
Line 4The final print shows r itself was modified; scale never made a copy.
Bound methods are ordinary objects
Each instance gives you a distinct bound method wrapping the same underlying function.
class Greeter:
def __init__(self, name):
self.name = name
def greet(self, greeting="Hello"):
return f"{greeting}, {self.name}!"
a = Greeter("Ada")
g = Greeter("Grace")
for call in (a.greet, g.greet):
print(call("Hi"))
print(a.greet is a.greet)
print(a.greet.__func__ is Greeter.greet)
print(a.greet.__self__.name)Example explained
Line 1a.greet can be stored in a tuple and called later; no lambda is needed to capture the instance.
Line 2a.greet is a.greet is False because a fresh bound method object is created on each attribute access.
Line 3__func__ is the single plain function stored on the class and shared by every instance.
Line 4__self__ is the instance the method will pass as its first argument.
What happens when self is missing
A method defined without a first parameter still receives the instance, so the call fails.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def to_fahrenheit(self):
return self.celsius * 9 / 5 + 32
def bad_bump():
return "no parameter to receive the instance"
t = Temperature(100)
print(t.to_fahrenheit())
try:
t.bad_bump()
except TypeError as exc:
print(type(exc).__name__, "- the instance was passed anyway")
print(Temperature.bad_bump())Example explained
Line 1to_fahrenheit reaches the stored value only because self names the instance.
Line 2t.bad_bump() binds t as the first argument, but bad_bump accepts zero parameters, hence TypeError.
Line 3Temperature.bad_bump() skips binding entirely, so the zero-argument function runs fine.
Line 4The lesson: the signature must have a slot for the instance, whatever you name it.
Important notes
self is a convention, not a keyword; a method whose first parameter is named this or obj works identically, but every reader and linter expects self.
A bound method keeps a strong reference to its instance through __self__, so storing obj.method in a long-lived list or callback registry keeps obj alive.
Common mistakes
Leaving self out of the parameter list: the instance is still passed at call time, so you get a TypeError about too many positional arguments the first time the method is used on an object.
Writing value += 1 instead of self.value += 1: Python creates or reads a local (or raises NameError / UnboundLocalError), and the object's state is never updated.
Calling Counter.increment() with no arguments through the class: nothing is bound, so self has no value and Python reports a missing required positional argument.
Try it yourself
Change, predict, then run
Write a Playlist class whose __init__ sets self.tracks = [], whose add(track) appends and returns self so calls chain, and whose summary() returns "3 tracks: a, b, c"; then confirm that Playlist.add(p, "Blue") changes p exactly like p.add("Blue") does.
Open the Python workspaceCheck your understanding
increment is defined as def increment(self, by=1), yet c.increment(5) supplies only one argument and works. Why?
- Looking up increment through the instance produces a bound method that supplies c as the first argument when called.
- self is a reserved parameter name, so the interpreter fills it in automatically wherever it appears.
- self is given a default value equal to the instance when the class body is executed.
- The compiler rewrites any call written with a dot into ClassName.method(instance, ...) before running it.
Show answer
Attribute access on an instance triggers the function's descriptor behaviour and returns a bound method that already holds the instance, which it prepends to the arguments on each call. Option 2 is tempting but wrong: self is only a convention, and a method whose first parameter is named anything else behaves the same, which proves the name plays no part in the mechanism.