Lesson 06

Functions and Argument Parsing

How a Python call reaches your C function, what shape the arguments arrive in, how to unpack them safely, and how to call back into Python.

Learning objective

You can choose the right calling convention for a function, parse arguments without creating dangling pointers, build return values, and invoke Python callables from C using the modern low-overhead APIs.

PyMethodDef: the table that exposes C to Python

Whether a C function becomes a module-level function or a method on a type, it is registered through the same struct:

typedef struct PyMethodDef {
    const char  *ml_name;   /* the Python-visible name */
    PyCFunction  ml_meth;   /* the C function pointer  */
    int          ml_flags;  /* calling convention      */
    const char  *ml_doc;    /* the docstring           */
} PyMethodDef;

Arrays of these are NULL-terminated and are assigned to m_methods on a module or tp_methods/Py_tp_methods on a type. The first parameter of every C function in the table is conventionally called self, and it is:

This is worth pausing on: for a module-level function, self is your module, which is how you reach per-module state in Lesson 08 without any globals.

Calling conventions

FlagsC signatureUse for
METH_NOARGS PyObject *f(PyObject *self, PyObject *unused) Zero-argument functions. The second parameter is always NULL; name it Py_UNUSED(ignored).
METH_O PyObject *f(PyObject *self, PyObject *arg) Exactly one positional argument. No parsing at all — the fastest possible entry point.
METH_VARARGS PyObject *f(PyObject *self, PyObject *args) Arguments arrive as a tuple. The classic convention. Requires building a tuple on every call.
METH_VARARGS | METH_KEYWORDS PyObject *f(PyObject *self, PyObject *args, PyObject *kwargs) Tuple plus dict (which may be NULL). The general-purpose choice.
METH_FASTCALL PyObject *f(PyObject *self, PyObject *const *args, Py_ssize_t nargs) Positional arguments as a C array. No tuple is allocated. Preferred for hot paths.
METH_FASTCALL | METH_KEYWORDS PyObject *f(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) Array of positionals followed by keyword values; kwnames is a tuple of the corresponding names, or NULL.
METH_METHOD | METH_FASTCALL | METH_KEYWORDS PyObject *f(PyObject *self, PyTypeObject *defining_class, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) Adds the class that defined the method, which is how a method on a heap type reaches module state (Lesson 08).

Modifier flags that combine with the above: METH_CLASS (classmethod), METH_STATIC (staticmethod), METH_COEXIST (allow this method to take precedence over a slot-generated wrapper of the same name).

Vectorcall, in one paragraph

The METH_FASTCALL layout — a pointer to a contiguous array of arguments plus a count plus an optional names tuple — is CPython's vectorcall protocol (PEP 590). The interpreter already has arguments laid out contiguously on its value stack, so passing a pointer into that stack costs nothing, whereas METH_VARARGS forces it to allocate a tuple, copy pointers in, and free it again on every call. For a function called in a loop this is a measurable fraction of the total cost. There is a matching consumer side, PyObject_Vectorcall, for calling out.

Caveat: the arguments in a METH_FASTCALL array are borrowed and the array itself is only valid for the duration of the call. Do not store either.

Choosing

PyArg_ParseTuple and friends

The parsing functions take a format string describing the expected arguments and a list of pointers to fill in. They return true on success, and on failure they return false with a suitable TypeError or ValueError already raised — so the caller just returns NULL.

static PyObject *
resize(PyObject *self, PyObject *args, PyObject *kwargs)
{
    static char *kwlist[] = {"width", "height", "mode", NULL};
    int width, height;
    const char *mode = "nearest";     /* default for the optional argument */

    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|s:resize", kwlist,
                                     &width, &height, &mode)) {
        return NULL;
    }
    ...
}

The kwlist array must have exactly one entry per format unit, in order, terminated by NULL. An empty string entry marks a positional-only parameter.

Format-string markers

|Everything after this is optional. Your C variables keep whatever value you initialized them to — so initialize them, since the parser does not touch them when the argument is absent.
$Everything after this is keyword-only. ParseTupleAndKeywords only.
:Ends the format; the rest is the function name used in error messages.
;Ends the format; the rest replaces the entire error message.

The format units you will actually use

UnitC typeAcceptsLifetime
i l L n Kint, long, long long, Py_ssize_t, unsigned long longint (via __index__)By value — safe.
f dfloat, doublefloatBy value — safe.
pintanything; result is 0/1 truthinessBy value — safe.
sconst char *str, encoded UTF-8, must contain no NULBorrowed into the str's internal cache.
s#const char *, Py_ssize_tstr or read-only bytes-like; NULs allowedBorrowed.
z, z#as ssame, plus None → NULL pointerBorrowed.
y, y#const char * (+len)read-only bytes-like, no strBorrowed.
s*, y*, w*Py_bufferany buffer-exporting object (w* requires writable)Locked view — you must call PyBuffer_Release.
UPyObject *str only, no conversionBorrowed reference.
OPyObject *anythingBorrowed reference.
O!PyTypeObject *, PyObject *instances of that type (subclasses allowed)Borrowed reference.
O&converter fn, void *whatever your converter acceptsWhatever your converter defines.
The lifetime rule for parsed arguments

Everything the parser hands you that is a pointer — s, y, z, O, U — is borrowed, kept alive only by the argument tuple, which is kept alive only for the duration of your call. Three consequences:

Custom converters with O&

O& calls a function of yours to turn the object into whatever C value you want, which keeps validation out of the function body:

/* Returns 1 on success, 0 on failure with an exception set.
   Return Py_CLEANUP_SUPPORTED instead of 1 to opt into cleanup calls. */
static int
convert_mode(PyObject *obj, void *addr)
{
    const char *s = PyUnicode_AsUTF8(obj);
    if (s == NULL) return 0;
    if (strcmp(s, "fast") == 0)      { *(int *)addr = MODE_FAST; return 1; }
    if (strcmp(s, "accurate") == 0)  { *(int *)addr = MODE_ACC;  return 1; }
    PyErr_Format(PyExc_ValueError, "unknown mode %R", obj);
    return 0;
}

/* usage: */
int mode = MODE_FAST;
PyArg_ParseTuple(args, "O&:go", convert_mode, &mode);

The lightweight alternative

When you only need "between 1 and 3 objects, no conversion", skip the format string:

PyObject *obj, *callback = NULL;
if (!PyArg_UnpackTuple(args, "ref", 1, 2, &obj, &callback)) return NULL;
/* both are borrowed references */

Argument Clinic

Writing PyArg_ParseTupleAndKeywords by hand is repetitive, is slower than it needs to be, and produces signatures that help() and inspect.signature cannot read. CPython's own solution is Argument Clinic, a preprocessor: you write a declarative signature in a comment block, run a tool, and it generates a fast keyword parser, the docstring, and the __text_signature__ that makes introspection work.

/*[clinic input]
mymod.resize

    width: int
    height: int
    mode: str = "nearest"

Resize the image.
[clinic start generated code]*/

It is a CPython-internal tool rather than a public one, but it ships with the source and is usable in third-party projects. You do not need it, and this course does not assume it — but if you find yourself hand-writing many keyword parsers, know it exists. Its most valuable output is the fast keyword parsing that PyArg_ParseTupleAndKeywords cannot provide for METH_FASTCALL functions.

Building return values

Py_BuildValue is the inverse of PyArg_ParseTuple and returns a new strong reference:

return Py_BuildValue("i", n);                 /* an int              */
return Py_BuildValue("(iid)", x, y, dist);    /* a 3-tuple           */
return Py_BuildValue("{s:i,s:s}", "n", n, "name", name);   /* a dict  */
return Py_BuildValue("y#", buffer, (Py_ssize_t)len);       /* bytes   */

Two units matter for ownership: O takes an object and increfs it, while N takes an object and steals your reference. Use N for something you just created and would otherwise have to decref:

/* Without N you would need a temporary and a Py_DECREF on both paths. */
return Py_BuildValue("(NN)", PyLong_FromLong(a), PyLong_FromLong(b));

For simple cases the direct constructors are clearer and faster: PyLong_FromLong, PyFloat_FromDouble, PyUnicode_FromString, PyUnicode_FromFormat, PyBytes_FromStringAndSize, PyTuple_Pack.

Calling back into Python

Wrapping a library almost always means invoking a Python callable from C. The modern, allocation-light APIs:

PyObject_CallNoArgs(fn)Zero arguments. No tuple built.
PyObject_CallOneArg(fn, arg)One argument. No tuple built.
PyObject_Vectorcall(fn, args, nargsf, kwnames)The general fast path: a C array of arguments.
PyObject_CallFunctionObjArgs(fn, a, b, NULL)Variadic, NULL-terminated, objects only.
PyObject_Call(fn, args_tuple, kwargs_dict)The fully general form when you already have a tuple and dict.
PyObject_CallMethodNoArgs(obj, name) / PyObject_CallMethodOneArgMethod calls without building a bound method object.
PyObject_CallFunction(fn, "is", n, s)Convenience using Py_BuildValue format. Convenient, not fast.

All of them return a new strong reference, or NULL with an exception set. Three obligations at every call site:

  1. Check for NULL. A Python callback can raise anything.
  2. Decref the result, even when you ignore it.
  3. Assume the world changed. The callback ran arbitrary code: it may have mutated your containers, released and reacquired the interpreter, imported modules, or triggered a garbage collection. Re-validate anything you cached across the call, and never hold a borrowed reference across one.
PyObject *res = PyObject_CallOneArg(state->callback, item);
if (res == NULL) {
    return -1;               /* propagate; exception already set */
}
Py_DECREF(res);
Hands-on

Measure the convention difference without writing C, using functions that already use each one:

import timeit

# METH_O (one arg, no parsing) vs METH_VARARGS-style parsing:
print(timeit.timeit("len(x)", setup="x=[1,2,3]", number=2_000_000))
print(timeit.timeit("divmod(7, 3)", number=2_000_000))

# Introspectability is a real consequence of the parsing choice:
import math, inspect
print(inspect.signature(math.hypot))   # Argument Clinic gave this a signature
help(math.pow)                          # note the rendered signature line

Then sketch, in C, a function chunk(data, size, *, pad=False) and decide: which convention, which format string, which units, and exactly where you must call PyBuffer_Release.

Further reading