Skip to main content

Objects/call.c

cpython 3.14 @ ab2d84fe1023/Objects/call.c

The call protocol dispatch layer. Every Python f(args) expression in the eval loop eventually routes through one of the public entry points here. PEP 590 (vectorcall, 3.8) is the dominant path: any type that advertises a tp_vectorcall slot receives calls as a C array plus a nargsf count, avoiding the heap allocation of a positional tuple. Types that only implement the older tp_call slot are bridged through _PyObject_MakeTpCall, which builds the expected tuple/dict shape and dispatches through tp_call.

The file also contains call_function_ex, the backend for the CALL_FUNCTION_EX opcode used when f(*args, **kwargs) unpacking is present. call_function_ex handles the normalization of the starred argument into a tuple and the double-star argument into a dict before handing off to PyObject_Call. A small _Py_CheckFunctionResult helper at the bottom enforces the contract that a call must either return a non-NULL value or leave a set exception, never both and never neither.

Map

LinesSymbolRolegopy
1-150PyObject_Call, _PyObject_Call, _PyObject_CallNoArgsTstateLegacy tp_call entry; validates callable, checks for vectorcall fast path, delegates; CallNoArgs is a zero-allocation convenience wrapper.objects/call.go:Call, CallNoArgs
150-350PyObject_Vectorcall, _PyObject_VectorcallTstate, PyVectorcall_Function, _PyObject_MakeTpCall, _PyVectorcall_CallCore vectorcall dispatch; MakeTpCall converts vectorcall args to tuple/dict for tp_call; _PyVectorcall_Call converts tuple/dict to vectorcall for tp_vectorcall.objects/call.go:Vectorcall, MakeTpCall, VectorcallDict
350-500call_function_ex, CALL_FUNCTION_EX helperNormalizes *args into a tuple, **kwargs into a dict, then calls PyObject_Call; used by the CALL_FUNCTION_EX opcode.objects/call.go:CallFunctionEx (via vm/eval_call.go)
500-600_Py_CheckFunctionResult, handle_func_event, convenience wrappers (PyObject_CallOneArg, PyObject_CallMethodObjArgs, _PyObject_VectorcallPrepend)Result invariant check; tracing hook dispatch; single-arg and method-call shortcuts.objects/call.go:CallOneArg, VectorcallPrepend

Reading

Vectorcall protocol (lines 150 to 350)

cpython 3.14 @ ab2d84fe1023/Objects/call.c#L150-350

PyObject_Vectorcall is the preferred entry point from the eval loop. The nargsf parameter carries the positional count in the low bits plus an optional PY_VECTORCALL_ARGUMENTS_OFFSET flag in the high bit. The flag signals that the caller reserved one extra slot at args[-1] so the callee can overwrite it with self without a new allocation:

PyObject *
_PyObject_VectorcallTstate(PyThreadState *tstate, PyObject *callable,
PyObject *const *args, size_t nargsf,
PyObject *kwnames)
{
assert(kwnames == NULL || PyTuple_Check(kwnames));
vectorcallfunc func = _PyVectorcall_FunctionInline(callable);
if (func == NULL) {
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
return _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames);
}
PyObject *res = func(callable, args, nargsf, kwnames);
return _Py_CheckFunctionResult(tstate, callable, res, NULL);
}

_PyVectorcall_FunctionInline checks tp_vectorcall_offset on the type and reads the slot from the object's memory at that offset. Types that do not expose the slot return NULL, routing to _PyObject_MakeTpCall.

_PyObject_MakeTpCall builds the legacy (args_tuple, kwargs_dict) shape. The keyword names come in as a kwnames tuple while their values live in the trailing slots of args. MakeTpCall constructs an args tuple from args[0:nargs] and a kwargs dict from zip(kwnames, args[nargs:]):

PyObject *
_PyObject_MakeTpCall(PyThreadState *tstate, PyObject *callable,
PyObject *const *args, Py_ssize_t nargs,
PyObject *keywords)
{
/* Build positional tuple. */
PyObject *argstuple = _PyTuple_FromArray(args, nargs);
if (argstuple == NULL) return NULL;

/* Build keyword dict from (kwnames, values) or existing dict. */
PyObject *kwdict = NULL;
if (keywords != NULL && PyTuple_GET_SIZE(keywords)) {
kwdict = _PyStack_AsDict(args + nargs, keywords);
if (kwdict == NULL) { Py_DECREF(argstuple); return NULL; }
}

ternaryfunc call = Py_TYPE(callable)->tp_call;
if (call == NULL) {
PyErr_Format(PyExc_TypeError,
"'%.200s' object is not callable",
Py_TYPE(callable)->tp_name);
Py_DECREF(argstuple);
Py_XDECREF(kwdict);
return NULL;
}
PyObject *result = call(callable, argstuple, kwdict);
Py_DECREF(argstuple);
Py_XDECREF(kwdict);
return _Py_CheckFunctionResult(tstate, callable, result, NULL);
}

_PyObject_MakeTpCall bridge (lines 200 to 260)

cpython 3.14 @ ab2d84fe1023/Objects/call.c#L200-260

The bridge is symmetrical in both directions. _PyVectorcall_Call (the inverse of MakeTpCall) converts an existing (tuple, dict) pair into a vectorcall so that calling tuple.__add__((1,2)) goes through vectorcall even though the caller started with a tuple. It extracts positional items from the tuple, appends keyword values from the dict, assembles a kwnames tuple, and dispatches through tp_vectorcall:

static PyObject *
_PyVectorcall_Call(PyThreadState *tstate, vectorcallfunc func,
PyObject *callable, PyObject *tuple, PyObject *kwargs)
{
Py_ssize_t nargs = PyTuple_GET_SIZE(tuple);
Py_ssize_t nkwargs = (kwargs == NULL) ? 0 : PyDict_GET_SIZE(kwargs);
Py_ssize_t totalargs = nargs + nkwargs;

PyObject *const *args;
PyObject *kwnames;
/* fast path: no kwargs */
if (nkwargs == 0) {
args = _PyTuple_ITEMS(tuple);
return func(callable, args, nargs, NULL);
}
/* slow path: pack kwargs into trailing slots + kwnames tuple */
PyObject **stack = PyMem_Malloc(totalargs * sizeof(PyObject *));
/* ... copy positionals, then iterate dict ... */
PyObject *res = func(callable, stack, nargs, kwnames);
PyMem_Free(stack);
return res;
}

call_function_ex (lines 350 to 500)

cpython 3.14 @ ab2d84fe1023/Objects/call.c#L350-500

call_function_ex handles the f(*args, **kwargs) case from the CALL_FUNCTION_EX opcode. It must normalize *args into a tuple (it might be any iterable) and merge any **kwargs dict into the keyword side. The merge step copies an existing dict if it is already the right shape:

static PyObject *
call_function_ex(PyThreadState *tstate, PyObject *func,
PyObject *callargs, PyObject *kwargs)
{
PyObject *result;
if (!PyTuple_Check(callargs)) {
/* Materialize any iterable into a tuple. */
callargs = PySequence_Tuple(callargs);
if (callargs == NULL) return NULL;
}
else {
Py_INCREF(callargs);
}

if (kwargs == NULL || PyDict_GET_SIZE(kwargs) == 0) {
result = PyObject_Call(func, callargs, NULL);
}
else {
if (!PyDict_CheckExact(kwargs)) {
/* Coerce a dict subclass to a plain dict to avoid __setitem__ side-effects. */
kwargs = PyDict_Copy(kwargs);
if (kwargs == NULL) { Py_DECREF(callargs); return NULL; }
result = PyObject_Call(func, callargs, kwargs);
Py_DECREF(kwargs);
}
else {
result = PyObject_Call(func, callargs, kwargs);
}
}
Py_DECREF(callargs);
return result;
}

gopy mirror

objects/call.go for Call, CallNoArgs, CallOneArg, CallObject, Vectorcall, VectorcallDict, VectorcallPrepend, MakeTpCall, and CallPrepend. The _Py_CheckFunctionResult invariant is enforced at the call sites in vm/eval_call.go rather than inside every dispatch function. CallFunctionEx lives in vm/eval_call.go because it needs the frame's namespace for **kwargs merging.

The VectorcallArgumentsOffset constant mirrors PY_VECTORCALL_ARGUMENTS_OFFSET from Include/cpython/abstract.h. The Go implementation does not exploit the offset optimization (writing into args[-1]) because Go slice bounds checking prevents addressing before the start of a sub-slice. VectorcallPrepend therefore always allocates a fresh stack, which is correct but slightly slower than the C path.

CPython 3.14 changes

The vectorcall protocol (PEP 590) shipped in 3.8. tp_vectorcall_offset was added to PyTypeObject in 3.8 and back-ported to PyCFunctionObject, PyMethodObject, and PyFunctionObject. handle_func_event (the sys.monitoring hook for CALL events) was added in 3.12. The _Py_CheckFunctionResult function has been present since 3.0 but was moved to call.c in 3.8 when vectorcall was introduced.