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 exceptionAt 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 " + nameThe class dictionary contains a function object:
print(User.__dict__["greet"])When accessed through an instance:
u = User()
u.greetthe function’s descriptor behavior binds the instance as self.
Conceptually:
User.__dict__["greet"].__get__(u, User)
-> bound methodThe bound method stores:
function object
self objectCalling:
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__ -> objYou 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.fdoes not bind a specific instance as self.
Conceptually:
C.__dict__["f"].__get__(None, C)
-> function object or descriptor result suitable for class accessSo 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.methodand 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 descriptorsMethod calls cannot be understood fully without descriptors.
32.6 Data vs Non-Data Descriptors
Descriptors come in two broad categories.
| Descriptor kind | Defines | Priority |
|---|---|---|
| Data descriptor | __get__ and __set__ or __delete__ | Higher than instance dictionary |
| Non-data descriptor | Usually 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 10The lookup rules affect what object is eventually called.
32.7 Attribute Lookup Order for Methods
For a typical instance attribute access:
obj.nameCPython 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 AttributeErrorA 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_VALUEThe 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 1If 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 objectand 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 methodThat 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.methodHere, 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 NoneThe 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:
[].appendit 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 functionSo 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 argumentThis 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 resultIf 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 objectSo 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 loadingThe 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 wrappersIt 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):
passThe 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() + 1The 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
callThe 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 0The 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 shapeOn the next execution:
if receiver type still matches
and type version is unchanged
use cached method path
else
fall back to generic lookupThis 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 trueDynamic 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.xSlots 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 + bmay 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 + otherare 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 raisedFor 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 resultIncorrect 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 lookupThen 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 bodyEach 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 indirectlyExample:
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 NoneThe 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 = objThis 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.fNow:
obj
-> callback bound method
-> self objThis 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 self32.42 Common Misunderstandings
| Misunderstanding | Correct model |
|---|---|
obj.method(x) directly calls a function stored on obj | It performs attribute lookup, binding, then call |
self is a keyword inserted by syntax | self is just the first argument by convention |
| Methods always live in instances | Normal methods live on classes and bind to instances |
| Bound methods are always allocated | CPython can avoid allocation for immediate calls |
staticmethod receives self | It receives no automatic first argument |
classmethod receives the instance | It receives the class |
| Special methods are always looked up like normal methods | Many are resolved through type slots |
| Method lookup is static | It 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 * 2and 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 exceptionCPython 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.