Python’s Vectorcall Protocol
Python’s vectorcall protocol is a CPython calling convention that passes positional and keyword arguments to a callable in a compact array instead of first packaging them into a tuple and dictionary. It reduces temporary object creation and lets CPython dispatch many function and method calls more directly.
This is an implementation detail of CPython, not a different kind of function visible in Python source. You use it every time ordinary Python code calls a function, constructor, or method; the protocol explains part of why those calls became cheaper in modern CPython releases.
The cost of an ordinary call
At the Python level, a call looks simple:
result = transform(value, limit=10)
Internally, the interpreter must convey value as a positional argument and 10 as a keyword argument. Before vectorcall, the general-purpose call path commonly represented these arguments as:
- a tuple containing positional arguments;
- a dictionary containing keyword names and values;
- a call through a generic type-level function, which then determined how to invoke the target.
Those containers are temporary for a normal call. Creating them means allocating objects, filling them, and later releasing them. The generic dispatch path also adds work before the target function starts running. For a large function, that overhead may be insignificant. For a tiny function called millions of times, it is easier to measure.
The old representation was also inconvenient for methods. An expression such as obj.method(x) could create a bound method object: a temporary object holding both the function and obj as its receiver. That object then had to be called immediately.
Vectorcall addresses these costs by making the common call shape explicit.
The vectorcall representation
A vectorcall-capable callable receives arguments through a C-level function pointer with a shape conceptually like this:
vectorcallfunc(callable, args, nargsf, kwnames)
The exact C declaration uses CPython types, but the important parts are:
callableis the object being called;argspoints to a contiguous array ofPyObject *pointers;nargsfcontains the number of positional arguments, plus a possible flag;kwnamesis a tuple of keyword-name strings, orNULLwhen there are no keywords.
The values for keyword arguments are stored in the same array immediately after the positional arguments. For example, this call:
result = transform(value, limit=10, mode="fast")
can be represented conceptually as:
args: [value, 10, "fast"]
kwnames: ("limit", "mode")
count: 1 positional argument
The first item in args is positional. The remaining items correspond by position to the names in kwnames. The arguments are borrowed references while the call is being processed: the receiving code does not automatically own a new reference to every item merely because it received the array.
This representation does not require a newly allocated tuple for positional arguments or a newly allocated dictionary for keywords. The array can often be the argument area the interpreter already has available while executing the call.
The count-and-flag value has one detail worth knowing. CPython reserves a bit called PY_VECTORCALL_ARGUMENTS_OFFSET. When set, it indicates that the callable may use the slot immediately before args for its own purposes, typically to support efficient forwarding or method-related call handling. The flag is part of the protocol’s low-level contract; ordinary Python code does not set it directly.
How dispatch changes
A vectorcall-capable object stores a pointer to its vectorcall function. CPython’s internal call machinery can check for that capability and invoke the pointer directly, rather than routing the call through the older generic tp_call slot and building argument containers first.
This is not dynamic compilation or a shortcut that bypasses argument checking. The target still validates the number of arguments, matches keyword names to parameters, applies defaults, and raises normal errors such as TypeError. Vectorcall changes how arguments arrive and how the callable is reached, not Python’s calling semantics.
Built-in functions, many built-in types, Python function objects, and other optimized callables can use this protocol. A C extension type can opt in by providing a vectorcall function and identifying where its vectorcall pointer is stored in the object layout. CPython then knows that instances of that type support the convention.
Why methods benefit especially
Method calls have two related optimizations. First, CPython can often recognize the method-call form and keep the receiver separate instead of constructing a bound method object. It can then place the receiver into the argument sequence as the first argument and invoke the underlying callable through vectorcall.
For a method such as:
items.append(value)
the method implementation needs items as its receiver and value as the explicit argument. The interpreter can arrange those values directly for the call. There is no need to materialize a temporary object representing “the append method bound to items” if the method is called immediately.
This optimization is associated with bytecode and lookup behavior such as the method-loading path, while vectorcall supplies the efficient calling convention used once the callable is ready. They solve adjacent parts of the same problem: avoid temporary method objects, then avoid temporary argument containers and generic dispatch.
What it means in practice
Vectorcall is most useful for many small calls, including loops that repeatedly invoke built-ins, constructors, Python functions, or methods. It is not a guarantee that every call allocates nothing. Argument unpacking, unusual callable objects, decorators, *args, **kwargs, and custom extension types can require additional work. A callable that does not support vectorcall falls back to another call path.
You normally do not need to change application code to use it. The performance improvement comes from CPython’s interpreter and object implementations. If a profile or release note mentions vectorcall, it is referring to this lower-level boundary: arguments remain in a flat, pointer-based layout for longer, and CPython can dispatch directly to code that understands that layout.
The result is modest overhead per call rather than a change in behavior—but modest overhead matters when the call itself is small and repeated at scale.