Skip to content

32. Method Calls

LOAD_METHOD and CALL_METHOD optimization, bound method objects, and the method cache.

Method calls are function calls that usually begin with attribute access. They are central to Python’s object model because most object behavior is exposed through methods.

A method call such as:

obj.method(arg)

looks like one operation, but CPython executes several conceptual steps:

load obj
look up attribute method
bind obj if needed
load arg
call the resolved callable
return result or raise exception

At the language level, this is attribute lookup followed by a call. At the CPython level, this path is optimized heavily because method calls are common.

32.1 Method Calls Are Attribute Access Plus Call

The source expression:

obj.method(10)

can be understood as:

tmp = obj.method
tmp(10)

This is the correct semantic model. The attribute lookup happens first. The result of that lookup is then called.

That result may be:

a bound method
a function
a built-in method
a callable object
a descriptor result
a property result
any object returned by __getattribute__

The call succeeds only if the resolved value is callable.

Example:

class C:
    value = 10

obj = C()
obj.value()

This fails because obj.value resolves to 10, and an integer is not callable.

32.2 Plain Function Stored on a Class

A function defined inside a class becomes a descriptor.

class User:
    def greet(self, name):
        return "hello " + name

The class dictionary contains a function object:

print(User.__dict__["greet"])

When accessed through an instance:

u = User()
u.greet

the function’s descriptor behavior binds the instance as self.

Conceptually:

User.__dict__["greet"].__get__(u, User)
    -> bound method

The bound method stores:

function object
self object

Calling:

u.greet("Ada")

is conceptually equivalent to:

User.greet(u, "Ada")

The automatic self argument is the main distinction between function calls and ordinary instance method calls.

32.3 Bound Methods

A bound method packages a function with an object.

class C:
    def f(self, x):
        return x + 1

obj = C()
m = obj.f

print(m(10))

Here, m is a bound method. It remembers obj.

Conceptually:

bound method
    __func__ -> C.f
    __self__ -> obj

You can inspect this:

print(m.__func__)
print(m.__self__)

When m(10) is called, CPython calls the underlying function with obj inserted as the first argument:

m.__func__(m.__self__, 10)

32.4 Unbound Function Access Through the Class

Accessing a method through the class gives a function-like object that does not bind an instance in the same way.

class C:
    def f(self, x):
        return x + 1

obj = C()

print(C.f(obj, 10))

Here, obj is passed explicitly.

The class access:

C.f

does not bind a specific instance as self.

Conceptually:

C.__dict__["f"].__get__(None, C)
    -> function object or descriptor result suitable for class access

So these are equivalent for normal instance methods:

obj.f(10)
C.f(obj, 10)

The first form performs instance binding. The second form passes the instance explicitly.

32.5 Descriptor Protocol

Method binding is part of the descriptor protocol.

A descriptor is an object with one or more of these methods:

__get__
__set__
__delete__

Functions are non-data descriptors because they define __get__.

When CPython evaluates:

obj.method

and finds a function object in the class, it calls the function’s descriptor logic.

Conceptually:

method = function.__get__(obj, type(obj))

This produces the bound method.

The descriptor protocol also powers:

methods
staticmethod
classmethod
property
slots
many built-in attributes
ORM fields
cached attributes
validation descriptors

Method calls cannot be understood fully without descriptors.

32.6 Data vs Non-Data Descriptors

Descriptors come in two broad categories.

Descriptor kindDefinesPriority
Data descriptor__get__ and __set__ or __delete__Higher than instance dictionary
Non-data descriptorUsually only __get__Lower than instance dictionary

Normal functions stored on classes are non-data descriptors.

This means an instance attribute can shadow a method:

class C:
    def f(self):
        return "method"

obj = C()
obj.f = lambda: "instance function"

print(obj.f())

The instance dictionary contains f, so it wins over the non-data descriptor in the class.

A property is a data descriptor, so it wins over the instance dictionary:

class C:
    @property
    def x(self):
        return 10

The lookup rules affect what object is eventually called.

32.7 Attribute Lookup Order for Methods

For a typical instance attribute access:

obj.name

CPython follows a lookup process roughly like this:

1. Determine type(obj)
2. Look for name in type and base classes
3. If a data descriptor is found, call its __get__
4. Otherwise, look in obj.__dict__ if present
5. If found in instance dictionary, return it
6. Otherwise, if a non-data descriptor was found, call its __get__
7. Otherwise, return the class attribute
8. Otherwise, call __getattr__ if defined
9. Otherwise, raise AttributeError

A method call depends on this lookup. The method is not selected by name alone. It is selected by the full attribute lookup protocol.

32.8 Method Call Bytecode

For:

def call(obj, x):
    return obj.method(x)

conceptual bytecode looks like:

LOAD_FAST obj
LOAD_METHOD method
LOAD_FAST x
CALL 1
RETURN_VALUE

The exact instructions vary by Python version, but modern CPython distinguishes method loading from generic attribute loading in common cases.

The goal is to optimize this pattern:

obj.method(arg)

without changing the semantic result.

32.9 Why LOAD_METHOD Exists

A naive method call would do this:

LOAD_FAST obj
LOAD_ATTR method
LOAD_FAST arg
CALL 1

If method is an ordinary function on the class, LOAD_ATTR creates a bound method object.

Then CALL immediately calls it.

That temporary bound method allocation is often unnecessary.

An optimized path can instead load:

underlying function
self object

and call the function directly with self inserted.

Conceptually:

obj.method(arg)

optimized:
    function = C.__dict__["method"]
    self = obj
    call function(self, arg)

This avoids creating a bound method object in the common immediate-call case.

32.10 Bound Method Allocation Avoidance

Consider a loop:

for item in items:
    obj.process(item)

A naive implementation could allocate one bound method per iteration:

obj.process -> new bound method
call bound method
discard bound method

That would create unnecessary allocation and reference count traffic.

CPython’s method-call optimization avoids this in common cases.

The semantic model remains:

tmp = obj.process
tmp(item)

But the implementation may skip materializing tmp as a heap object when the method is called immediately.

32.11 When Bound Methods Are Still Created

A bound method object is still created when method access is itself the result.

Example:

m = obj.method

Here, Python code asks for the attribute value. CPython must produce the bound method object because the program may store it, inspect it, pass it around, or call it later.

callbacks.append(obj.method)

In this case, the bound method object is the correct Python-visible result.

Optimization applies mainly to immediate call patterns where the bound method does not need to escape.

32.12 Built-in Methods

Built-in types expose many methods implemented in C.

xs = []
xs.append(1)

The list method append is implemented in C.

A built-in method call involves:

method lookup
binding to list object
argument setup
C method call
mutation of list object
return None

The bound object is still conceptually present, but the implementation can be highly optimized.

For list append, the operation mutates the underlying list structure directly in C.

32.13 Method Descriptors

Built-in methods are often represented by descriptor objects rather than ordinary Python function objects.

Example:

print(list.__dict__["append"])

This is not a normal Python function. It is a built-in method descriptor.

When accessed through an instance:

[].append

it produces a built-in method object bound to that list.

When called immediately:

[].append(1)

CPython can follow a specialized native path.

32.14 staticmethod

staticmethod disables instance binding.

class Math:
    @staticmethod
    def add(a, b):
        return a + b

Math.add(2, 3)
Math().add(2, 3)

In both cases, no self is inserted.

The descriptor returns the underlying function without binding an instance.

Conceptually:

staticmethod.__get__(obj, cls)
    -> original function

So this works:

Math().add(2, 3)

because the function receives exactly two arguments, not three.

32.15 classmethod

classmethod binds the class rather than the instance.

class C:
    @classmethod
    def make(cls, value):
        return cls(value)

Calling:

C.make(10)

passes C as the first argument.

Calling through an instance:

obj = C()
obj.make(10)

also passes the class, usually C, not obj.

Conceptually:

classmethod.__get__(obj, cls)
    -> bound method with cls as first argument

This is why class methods are useful for alternate constructors and polymorphic construction.

32.16 property and Method-Like Access

A property turns method logic into attribute access.

class C:
    @property
    def value(self):
        return 42

obj = C()
print(obj.value)

This does not call obj.value().

The call happens during attribute access:

obj.value
    property.__get__(obj, C)
        calls getter function
        returns result

If the property returns a callable, then a later call can happen:

obj.factory()

If factory is a property, this means:

call property getter
call returned object

So one source-level call can involve hidden descriptor calls before the explicit call.

32.17 __getattribute__

Every normal attribute lookup goes through __getattribute__.

class C:
    def __getattribute__(self, name):
        print("lookup", name)
        return super().__getattribute__(name)

    def f(self):
        return 1

obj = C()
obj.f()

The expression obj.f() first calls obj.__getattribute__("f").

This means method calls can be intercepted.

A custom __getattribute__ can:

return a normal bound method
return a different callable
return a non-callable
raise AttributeError
log access
implement proxies
implement lazy loading

The call machinery does not know the original source intent. It calls whatever attribute lookup returns.

32.18 __getattr__

__getattr__ is called only after normal lookup fails.

class Dynamic:
    def __getattr__(self, name):
        if name == "run":
            return lambda: "dynamic"
        raise AttributeError(name)

obj = Dynamic()
print(obj.run())

Here, run does not exist in the instance or class. __getattr__ returns a callable. The call then invokes that returned callable.

This is common in:

proxies
RPC clients
ORM models
mock objects
lazy APIs
dynamic wrappers

It also means method calls can be resolved dynamically at runtime.

32.19 Method Calls on Modules

Modules can also support dynamic attribute access with module-level __getattr__.

# module.py
def __getattr__(name):
    if name == "run":
        return lambda: 42
    raise AttributeError(name)

Then:

import module
module.run()

can resolve dynamically.

The lookup path differs from instance method binding, but the source pattern is still attribute access followed by call.

32.20 Method Resolution Order

For class instances, method lookup searches the class and its base classes using the method resolution order.

class A:
    def f(self):
        return "A"

class B(A):
    pass

obj = B()
print(obj.f())

The lookup searches B, then A.

For multiple inheritance:

class A:
    def f(self):
        return "A"

class B:
    def f(self):
        return "B"

class C(A, B):
    pass

The selected method depends on C.__mro__.

print(C.__mro__)

The MRO is central to method calls because it determines where class attribute lookup finds the descriptor.

32.21 super() Method Calls

super() changes where method lookup begins in the MRO.

class Base:
    def f(self):
        return 1

class Child(Base):
    def f(self):
        return super().f() + 1

The call:

super().f()

does not mean “call the parent class by name.” It means search the MRO after the current class, with binding to the current instance.

Conceptually:

current class = Child
instance = self
MRO = [Child, Base, object]
search after Child
find Base.f
bind to self
call

The result is a bound method using the same instance.

32.22 Methods and Inheritance

A method call may use a method inherited from a base class.

class Base:
    def save(self):
        return "saved"

class User(Base):
    pass

u = User()
u.save()

The lookup finds save in Base, then binds it to u.

Conceptually:

find Base.__dict__["save"]
bind with self = u
call Base.save(u)

The function’s defining class and the instance’s actual class can differ. This is normal.

32.23 Overriding Methods

Subclass methods override base methods.

class Base:
    def f(self):
        return "base"

class Child(Base):
    def f(self):
        return "child"

print(Child().f())

Lookup finds Child.f before Base.f.

This is runtime lookup. The call is not statically bound by the variable type.

def call_f(obj):
    return obj.f()

The method chosen depends on type(obj) at runtime.

32.24 Polymorphic Method Calls

Python method calls are dynamically dispatched.

def speak(animal):
    return animal.speak()

Different objects can provide different implementations:

class Dog:
    def speak(self):
        return "woof"

class Cat:
    def speak(self):
        return "meow"

The same bytecode can call different methods depending on the runtime object.

Conceptually:

LOAD_FAST animal
LOAD_METHOD speak
CALL 0

The actual target is discovered during execution.

This dynamic dispatch is flexible but creates optimization challenges.

32.25 Monomorphic and Polymorphic Call Sites

A method call site is monomorphic if it usually sees one object type.

for user in users:
    user.validate()

If every user has the same type, the call site is monomorphic.

A call site is polymorphic if it sees several types:

for shape in shapes:
    shape.area()

where shape may be Circle, Square, or Triangle.

Inline caches work best when call sites are stable. A monomorphic call site can cache type and method lookup information more effectively.

32.26 Inline Caches for Method Calls

CPython can cache method lookup information near the bytecode instruction.

A method cache may record facts like:

expected receiver type
type version tag
resolved descriptor
method object or function pointer
offset or lookup result
call shape

On the next execution:

if receiver type still matches
and type version is unchanged
    use cached method path
else
    fall back to generic lookup

This speeds up repeated calls without changing semantics.

If a class is modified, version tags or cache guards invalidate the fast path.

32.27 Class Mutation and Cache Invalidation

Python allows classes to change at runtime.

class C:
    def f(self):
        return 1

obj = C()
print(obj.f())

def new_f(self):
    return 2

C.f = new_f
print(obj.f())

The second call must use the new method.

Therefore, any cache that assumed the old method must be invalidated or guarded.

The safety rule:

fast method path is valid only while class and lookup assumptions remain true

Dynamic class mutation is one reason CPython optimization uses guards.

32.28 Instance Dictionary Mutation

Instance attributes can also affect method lookup for non-data descriptors.

class C:
    def f(self):
        return "class method"

obj = C()
obj.f = lambda: "instance value"

print(obj.f())

The instance dictionary entry shadows the class function because normal functions are non-data descriptors.

A method cache must account for instance dictionary state when relevant.

This is another reason method lookup is more complex than a direct class-table jump.

32.29 Slots and Method Calls

Classes with __slots__ may not have a normal instance dictionary.

class C:
    __slots__ = ("x",)

    def f(self):
        return self.x

Slots affect attribute storage, but method lookup still searches the class and its bases.

The absence of __dict__ can simplify some attribute cases, but descriptors, inheritance, and dynamic class mutation still matter.

32.30 Special Methods

Special methods such as __len__, __add__, and __iter__ are often looked up through type slots rather than ordinary instance attribute lookup.

Example:

len(obj)

does not simply execute:

obj.__len__()

in all respects. CPython typically uses the type’s slot for length.

This distinction matters:

class C:
    def __len__(self):
        return 10

obj = C()
obj.__len__ = lambda: 20

print(len(obj))
print(obj.__len__())

The explicit method call may use the instance attribute. The len() operation uses special method lookup through the type.

Special methods are optimized and integrated into object protocol slots.

32.31 Operator Calls vs Method Calls

Operators often map to special methods.

a + b

may call:

a.__add__(b)
b.__radd__(a)

But CPython does not perform ordinary obj.__add__ attribute lookup for every addition. It uses numeric slots on type objects.

So:

obj.method()

and:

obj + other

are both dynamic dispatch, but they use different internal paths.

Method calls use attribute lookup and call machinery. Operators use protocol slots, with fallback behavior.

32.32 Method Calls and Reference Counts

A method call must keep alive:

receiver object
resolved callable
arguments
temporary bound method, if created
return value
exception state, if raised

For an optimized method call that avoids a bound method object, CPython still needs to ensure the receiver remains alive while the underlying function runs.

Conceptually:

load receiver
resolve method
prepare self and args
call
release temporaries
push result

Incorrect reference handling here can cause severe bugs because method calls often reenter Python and arbitrary code can run.

32.33 Method Calls Can Reenter Python

Method lookup itself can execute Python code.

Examples:

custom __getattribute__
descriptor __get__
property getter
__getattr__
metaclass attribute lookup

Then the resolved method call can execute more Python code.

A single source expression:

obj.method(arg)

can involve:

call __getattribute__
call descriptor __get__
call method body

Each call can raise, mutate state, or change future lookup behavior.

32.34 Method Calls and Exceptions

A method call can fail at multiple points:

receiver expression raises
attribute lookup raises AttributeError or another exception
descriptor binding raises
argument expression raises
resolved object is not callable
argument binding fails
method body raises
return cleanup raises indirectly

Example:

obj.missing()

fails during attribute lookup.

Example:

obj.method(bad())

may fail while evaluating the argument before the method is called.

Example:

obj.method()

may fail inside the method body.

The bytecode error path must clean temporary stack values in all cases.

32.35 Method Calls and None

A common error:

xs = []
result = xs.append(1)
result.append(2)

list.append returns None.

The second line fails because result is None, not the list.

At the method-call level:

xs.append(1)
    mutates xs
    returns None

The return value is still pushed by the call instruction, then stored in result.

Method calls do not imply fluent chaining unless the method explicitly returns self or another object.

32.36 Chained Method Calls

Chained calls execute left to right.

obj.a().b().c()

Conceptually:

tmp1 = obj.a()
tmp2 = tmp1.b()
tmp3 = tmp2.c()

Each call returns the receiver for the next attribute lookup.

If a() returns None, then .b() fails.

Bytecode uses the stack to hold each intermediate result long enough to access the next method.

32.37 Fluent APIs

Some APIs deliberately return self:

class Builder:
    def set_name(self, name):
        self.name = name
        return self

    def set_age(self, age):
        self.age = age
        return self

builder = Builder().set_name("Ada").set_age(37)

Each method mutates the object and returns it.

The method-call machinery is ordinary. Fluent style is a library convention, not a special interpreter feature.

32.38 Methods as First-Class Objects

Methods can be stored and passed.

class C:
    def f(self, x):
        return x + 1

obj = C()
callback = obj.f

print(callback(10))

The bound method keeps obj alive.

Conceptually:

callback
    function = C.f
    self = obj

This can affect memory lifetime:

callbacks.append(obj.method)

The callback list now keeps the object alive through the bound method.

32.39 Method Calls and Garbage Collection

Bound methods can participate in reference graphs.

Example:

class C:
    def f(self):
        return 1

obj = C()
obj.callback = obj.f

Now:

obj
    -> callback bound method
        -> self obj

This creates a cycle.

CPython’s cyclic garbage collector can collect such cycles if they become unreachable and if finalization rules allow cleanup.

This is a practical example of how method objects connect to memory management.

32.40 Inspecting Methods

You can inspect method objects:

class C:
    def f(self, x):
        return x + 1

obj = C()
m = obj.f

print(type(m))
print(m.__func__)
print(m.__self__)

You can inspect class dictionary contents:

print(C.__dict__["f"])

You can inspect bytecode:

import dis

def call(obj, x):
    return obj.f(x)

dis.dis(call)

This lets you see whether the compiler emits method-specific instructions for your Python version.

32.41 A Minimal Method Binding Model

A toy descriptor model:

class Function:
    def __init__(self, code):
        self.code = code

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return BoundMethod(self, obj)

    def __call__(self, *args):
        return self.code(*args)

class BoundMethod:
    def __init__(self, func, self_obj):
        self.__func__ = func
        self.__self__ = self_obj

    def __call__(self, *args):
        return self.__func__(self.__self__, *args)

Use it:

def body(self, x):
    return self.value + x

class C:
    value = 10

C.f = Function(body)

obj = C()
print(obj.f(5))

This is not CPython’s implementation, but it captures the binding idea:

function stored on class
access through instance
descriptor creates bound method
call inserts self

32.42 Common Misunderstandings

MisunderstandingCorrect model
obj.method(x) directly calls a function stored on objIt performs attribute lookup, binding, then call
self is a keyword inserted by syntaxself is just the first argument by convention
Methods always live in instancesNormal methods live on classes and bind to instances
Bound methods are always allocatedCPython can avoid allocation for immediate calls
staticmethod receives selfIt receives no automatic first argument
classmethod receives the instanceIt receives the class
Special methods are always looked up like normal methodsMany are resolved through type slots
Method lookup is staticIt depends on runtime type, MRO, descriptors, and instance state

32.43 Reading Strategy

To study method calls, start with this program:

class C:
    def f(self, x):
        return x + 1

def call(obj):
    return obj.f(10)

Inspect:

import dis

dis.dis(call)

obj = C()
m = obj.f

print(m.__func__)
print(m.__self__)
print(C.__dict__["f"])

Then vary the class:

@staticmethod
def s(x): ...

@classmethod
def c(cls, x): ...

@property
def p(self): ...

Also test:

obj.f = lambda x: x * 2

and inspect how lookup changes.

This reveals the interaction between descriptors, instance dictionaries, bytecode, and call machinery.

32.44 Chapter Summary

A method call is attribute lookup followed by a call. For ordinary instance methods, function descriptors bind the receiver object as the first argument, producing a bound method conceptually equivalent to calling the class function with the instance inserted.

The core model is:

obj.method(arg)
lookup "method" on obj
apply descriptor binding if needed
obtain callable
call callable with arguments
return result or raise exception

CPython optimizes this path heavily. It can avoid temporary bound method allocation, cache method lookups, specialize stable call sites, and call built-in methods through fast C paths.

The semantics remain dynamic. Runtime type, MRO, descriptors, instance dictionaries, custom attribute hooks, class mutation, and special method rules all affect what method is actually called.