Python call Method Execution Flow Explained
When you invoke a Python object as if it were a function, Python
relies on the __call__ method to execute that request. This
article details the internal execution flow of invoking an instance,
explaining how the runtime translates call syntax into bytecode, why it
bypasses the instance dictionary to look up the method on the class, how
the CPython tp_call slot manages the execution, and how
control returns to the caller.
1. Bytecode Generation
The execution begins when the Python interpreter compiles the call
expression instance(*args, **kwargs). At the bytecode
level, this syntax generates a call instruction—such as
CALL in Python 3.11+ or CALL_FUNCTION in
earlier versions. The interpreter evaluates the callable object and its
arguments, pushing them onto the evaluation stack.
2. Bypassing the Instance Dictionary
Unlike standard attribute lookups, special methods (dunder methods)
bypass the instance's __dict__ entirely. Python does not
check instance.__dict__['__call__']. Instead, it retrieves
the type of the instance using type(instance).
This design choice ensures performance optimization and consistency:
- It prevents shadowed behavior where an instance variable named
__call__would unintentionally alter the callable status of an instance. - It allows the CPython runtime to look directly at the underlying C-level type structure.
3. Resolving the
tp_call Slot in CPython
In the CPython implementation, all Python types are represented by a
PyTypeObject struct. This struct contains predefined
function pointers known as "type slots."
- The slot responsible for callable behavior is
tp_call. - When a class defines a Python-level
__call__method, CPython populates the class'stp_callslot with a wrapper function namedslot_tp_call. - If
__call__is not defined anywhere in the class hierarchy,tp_callremainsNULL. If the interpreter detects aNULLslot, it immediately raises aTypeError: 'ClassName' object is not callable.
4. Method Binding and Argument Passing
Once Python confirms that tp_call is present, it invokes
slot_tp_call. This function handles the conversion between
the internal C representation and Python's call mechanics:
- It retrieves the Python function object corresponding to
__call__from the class hierarchy. - It prepends the target instance as the first argument
(
self), transforming the call intoClass.__call__(instance, *args, **kwargs). - It packages positional arguments as a tuple and keyword arguments as a dictionary.
5. Execution of the Method Body
With the arguments bound, Python creates a new frame on the call
stack for the __call__ method. The interpreter switches
context to execute the bytecode inside the method's body, executing
arbitrary logic defined by the user.
6. Value Return and Stack Cleanup
When the method completes:
- The returned object is placed onto the caller's evaluation stack. If
no explicit return statement exists,
Noneis returned. - If an unhandled exception occurs inside
__call__, the stack unwinds and propagates the exception up to the caller. - The execution frame for
__call__is destroyed, and execution resumes in the parent scope directly after the call instruction.