Fundamentals 10 min read

How Python Calls Functions Under the Hood

This article explains the low‑level mechanics of Python function calls, distinguishing Python‑implemented and C‑implemented functions, dissecting the bytecode generated for a simple call, and walking through the CPython CALL instruction, stack layout, method handling, and the relationship between PyFunctionObject, PyFrameObject, and PyCodeObject.

Satori Komeiji's Programming Classroom
Satori Komeiji's Programming Classroom
Satori Komeiji's Programming Classroom
How Python Calls Functions Under the Hood

In the previous article we examined how Python functions are represented internally, showing that a Python‑implemented function is an instance of PyFunctionObject whose type object is <class 'function'>, while a C‑implemented function or method is an instance of PyCFunctionObject with type <class 'builtin_function_or_method'>.

Python‑implemented functions are created with the def keyword, whereas built‑in functions such as sum or "".join are C‑implemented. The following snippet demonstrates the type differences:

def foo():
    pass

class A:
    def foo(self):
        pass

print(type(foo))               # <class 'function'>
print(type(A().foo))           # <class 'method'>
print(type(sum))                # <class 'builtin_function_or_method'>
print(type("".join))          # <class 'builtin_function_or_method'>

To see how a function call is compiled, we use the dis module on a simple function foo(a, b) and its invocation foo(1, 2). The disassembly shows the generated bytecode, including the CALL instruction that performs the actual call:

import dis
code_string = """

def foo(a, b):
    return a + b

foo(1, 2)
"""
dis.dis(compile(code_string, "<file>", "exec"))

The resulting bytecode (excerpt) is:

0 RESUME                     0
  2 LOAD_CONST                 0 (<code><code object foo at 0x7f...></code>)
  4 MAKE_FUNCTION               0
  6 STORE_NAME                 0 (foo)
  8 PUSH_NULL
 10 LOAD_NAME                  0 (foo)
 12 LOAD_CONST                 1 (1)
 14 LOAD_CONST                 2 (2)
 16 CALL                       2
 24 POP_TOP
 26 RETURN_CONST               3 (None)

The CALL opcode triggers the logic defined in CPython's TARGET(CALL) implementation. The interpreter first builds a stack layout where the bottom element is a NULL placeholder, followed by the callable object and its arguments. The NULL is introduced by the preceding PUSH_NULL instruction and serves two purposes: it reserves space for the return value and unifies the stack shape for both plain function calls and method calls.

TARGET(CALL) {
    // runtime stack (bottom → top): NULL, function, arg1, arg2, ...
    PyObject **args = (stack_pointer - oparg);
    PyObject *callable = stack_pointer[-(1 + oparg)];
    PyObject *method = stack_pointer[-(2 + oparg)];
    int is_meth = method != NULL;
    int total_args = oparg;
    // ... handle method binding, adjust args, total_args ...
    if (is_meth) {
        callable = method;
        args--;
        total_args++;
    }
    // ... vectorcall handling for Python functions ...
    res = PyObject_Vectorcall(callable, args,
        total_args | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames);
    // clean up stack and return
}

If the callable is a Python function (type &PyFunction_Type) and the interpreter is in the classic evaluation mode, the call is inlined via _PyFunction_Vectorcall. A new interpreter frame is created with _PyEvalFramePushAndInit, locals are set based on the code object's co_flags, and the frame is dispatched. Otherwise, the generic PyObject_Vectorcall is used.

Finally, the article clarifies the relationship between PyFunctionObject, PyFrameObject, and PyCodeObject. While a PyFunctionObject packages a PyCodeObject together with its global namespace, the actual execution is driven by a PyFrameObject created from the same PyCodeObject. Thus the function object disappears once the frame starts executing, and the frame and code object remain tightly coupled.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

bytecodeinterpreterfunction callPyCFunctionObjectpyfunctionobject
Satori Komeiji's Programming Classroom
Written by

Satori Komeiji's Programming Classroom

Python and Rust developer; I write about any topics you're interested in. Follow me! (#^.^#)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.