Instance Methods

Instance methods are the ordinary functions-on-objects that make up most of a class's behavior - the mechanics of self and binding explain exactly how obj.method() knows which object to act on.

An instance method is a function defined inside a class that operates on a specific instance's state, accessed through its first parameter, conventionally named self. Understanding how self gets filled in automatically - and what a "bound method" actually is - demystifies one of the most-used but least-explained parts of Python OOP.

self and Instance State

By convention (not a language requirement - self is just a name), the first parameter of an instance method refers to the specific instance the method was called on. Inside the method, self.attr reads or writes that instance's own state.

class Counter:
    def __init__(self) -> None:
        self.count = 0

    def increment(self, by: int = 1) -> None:
        self.count += by

c = Counter()
c.increment()
c.increment(5)
print(c.count)   # 6

How Method Calls Work

c.increment(5) and Counter.increment(c, 5) do exactly the same thing. Python looks up increment on the class, then automatically supplies c as the first argument (self) because you accessed it through an instance rather than through the class directly.

c = Counter()
c.increment(5)              # instance call - self is filled in automatically
Counter.increment(c, 5)      # equivalent explicit call - self passed manually
print(c.count)                # 10

Bound Methods

When you access c.increment (without calling it), Python doesn't just hand you the plain function - it produces a *bound method* object, which packages the function together with c as the instance it will apply self to. This is why the bound method can be called later, elsewhere, with no explicit self, and still act on the right object.

Accessing a method through an instance produces a bound method (instance baked in); accessing it through the class gives the plain underlying function.

c = Counter()
bound = c.increment       # a bound method object, remembers c
print(bound)                # <bound method Counter.increment of <...Counter object...>>
bound(3)                     # equivalent to c.increment(3)
print(c.count)                # 3

unbound = Counter.increment  # accessed via the class: just a plain function
print(unbound)                 # <function Counter.increment at ...>

The Descriptor Mechanism (Brief)

This binding happens because functions implement the descriptor protocol (__get__): accessing instance.method triggers the function's __get__, which returns a new bound-method object wrapping the function and the instance. You don't need to implement this yourself - it's automatic for every function defined in a class body - but knowing it exists explains why the behavior is consistent and not special-cased magic.

Passing Bound Methods Around

Because a bound method already carries its instance, it can be passed as a plain callable - to map(), as a callback, stored in a list - and it will still operate on the correct object when eventually called.

counters = [Counter(), Counter()]
incrementers = [c.increment for c in counters]   # list of bound methods
incrementers[0](10)
incrementers[1](20)
print(counters[0].count, counters[1].count)   # 10 20

Instance Methods Can Call Other Methods on self

Within a method, calling self.other_method() is a normal, common pattern for composing behavior - it works exactly like any other method call, with self supplying the same instance.

class Order:
    def __init__(self, items: list[float]) -> None:
        self.items = items

    def subtotal(self) -> float:
        return sum(self.items)

    def total_with_tax(self, rate: float) -> float:
        return self.subtotal() * (1 + rate)   # self.subtotal() calls another instance method

o = Order([10.0, 20.0])
print(o.total_with_tax(0.1))   # 33.0

Rules and Behavior: self Must Be Explicit in the Signature

Unlike some languages where this/self is implicit, Python requires every instance method's signature to explicitly declare its first parameter (usually named self) - the language does not hide it. Forgetting it, or calling the method incorrectly, produces a clear TypeError about argument counts.

Python still passes b as an argument automatically (because greet was accessed through an instance) - the function just wasn't written to accept it.

class Bad:
    def greet():          # missing self
        return 'hi'

b = Bad()
b.greet()   # TypeError: greet() takes 0 positional arguments but 1 was given

Practical Usage: Encapsulating Behavior with Data

Instance methods are the primary way to give a class behavior that naturally belongs with its data - validation, computed values, and actions that read or mutate the instance's own state, as opposed to class methods (operate on the class) or static methods (need neither).

class Rectangle:
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def scale(self, factor: float) -> None:
        self.width *= factor
        self.height *= factor

r = Rectangle(4, 5)
r.scale(2)
print(r.area())   # 80.0

Edge Cases

Two bound methods from the same underlying function but different instances compare unequal, but two bound methods obtained separately from the *same* instance and same function compare equal (they wrap the same function and instance, even though they're different bound-method objects in memory).

c = Counter()
print(c.increment == c.increment)   # True - same function + same instance
print(c.increment is c.increment)     # False - a new bound method object each access

c2 = Counter()
print(c.increment == c2.increment)    # False - different instances

Key Takeaways

self is just a convention (not a keyword); it refers to the instance a method was called on and is always the first parameter.

obj.method(args) is equivalent to Class.method(obj, args) - the instance is supplied automatically because access happened through the instance.

Accessing a method through an instance produces a bound method object that carries the instance with it, so it can be stored or passed around and still act correctly.

Every instance method must explicitly declare self in its signature; Python does not hide it.

Instance methods are how a class attaches behavior directly to per-instance state.

class Bad: def greet(): return 'hi' Bad().greet() # TypeError: too many arguments class Good: def greet(self): return 'hi' Good().greet()

Every instance method needs an explicit first parameter (self) to receive the instance that Python automatically supplies when calling through an instance.

class Counter: def increment(self): count += 1 # NameError - not using self.count class Counter: def increment(self): self.count += 1

Instance attributes must be accessed through self; a bare name like count refers to a local/global variable, not the object's attribute.

callback = Counter.increment # unbound: the plain function, no instance attached callback() # TypeError: missing 'self' argument c = Counter() callback = c.increment # bound method: instance is baked in callback() # works - calls c.increment()

Accessing a method via the class gives the plain function (no instance attached); accessing it via an instance gives a bound method that already carries self.

Is self a reserved keyword in Python?

No - self is purely a naming convention. Any name would work as the first parameter of an instance method, but self is universally used and should always be followed for readability.

What is the relationship between obj.method(args) and Class.method(obj, args)?

They are equivalent; obj.method(args) is shorthand where Python automatically supplies obj as the first argument (self) because the method was accessed through an instance.

What is a bound method?

An object produced when you access a method through an instance (e.g. obj.method) that packages the underlying function together with that specific instance, so it can be called later or passed around and will still operate on the correct object.

Why does Bad().greet() raise a TypeError if greet is defined as def greet(): ... with no self parameter?

Because Python still automatically supplies the instance as an argument when a method is accessed through an instance; a function with no parameter to receive it raises TypeError for having too many positional arguments.

What is different about Counter.increment versus c.increment (where c is a Counter instance)?

Counter.increment (accessed via the class) is the plain, unbound function requiring self to be passed explicitly; c.increment (accessed via the instance) is a bound method with c already attached as self.

Interview: why can a bound method be stored in a list or passed as a callback and still work correctly later?

Because the bound method object carries a reference to its instance alongside the function; calling it later supplies that stored instance as self automatically, regardless of where or when the call happens.

Can an instance method call another instance method on the same object?

Yes - within a method body, self.other_method() is a normal and common way to compose behavior, calling another method on the same instance.

Write a class TextBuffer with instance methods write(text) (appends text) and read() (returns the accumulated text).

class TextBuffer: def __init__(self) -> None: self._content = '' def write(self, text: str) -> None: self._content += text def read(self) -> str: return self._content b = TextBuffer() b.write('Hello, ') b.write('world!') print(b.read()) # Hello, world!

Each write() call mutates the instance's own _content attribute via self; read() returns that same instance's accumulated state.

What does this print, and why doesn't del b break the call to fn()?

42 fn is a bound method that holds its own reference to the Box instance internally; del b only removes the name b from the current scope, but the object itself stays alive as long as something (fn) still references it, so fn() still works.

Write a class Vector2D with x, y and an instance method add(other) that returns a new Vector2D representing the sum, without mutating either operand.

class Vector2D: def __init__(self, x: float, y: float) -> None: self.x = x self.y = y def add(self, other: 'Vector2D') -> 'Vector2D': return Vector2D(self.x + other.x, self.y + other.y) def __repr__(self) -> str: return f'Vector2D({self.x}, {self.y})' v1 = Vector2D(1, 2) v2 = Vector2D(3, 4) print(v1.add(v2)) # Vector2D(4, 6) print(v1, v2) # Vector2D(1, 2) Vector2D(3, 4) - both unchanged add() reads self and other's attributes to compute a fresh Vector2D without reassigning self.x/self.y or other.x/other.y, so neither input vector is mutated.

class Box:
    def __init__(self, value):
        self.value = value

    def get(self):
        return self.value

b = Box(42)
fn = b.get
del b
print(fn())