Skip to content

gh-158660: Add PyListWriter C API - #160127

Draft
vstinner wants to merge 6 commits into
python:mainfrom
vstinner:list_writer
Draft

vstinner wants to merge 6 commits into
python:mainfrom
vstinner:list_writer

Conversation

@vstinner

@vstinner vstinner commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

@vstinner

Copy link
Copy Markdown
Member Author

If a list writer is used by multiple threads, listwriter_resize() can call _PyObject_GC_SET_SHARED() on Free Threading.

@vstinner

Copy link
Copy Markdown
Member Author

This C API is just an abstraction on top of existing PyList functions. PyListWriter_Create() doesn't allocate a PyListWriter structure. In fact, it's just a PyListObject* pointer! It is the same design than the PyLongWriter API.

@read-the-docs-community

read-the-docs-community Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

@vstinner

Copy link
Copy Markdown
Member Author

Another advantage of using this C API instead of PyList_New() is that the Python list object is only tracked by the GC when PyListWriter_Finish() is called. You cannot see the uninitialized list using the gc module.

@vstinner

Copy link
Copy Markdown
Member Author

Tests / Hypothesis tests on Ubuntu (pull_request): Failing after 10m

Oh, this failure seems to be unrelated to my change:

FAIL: test_system_site_packages (test.test_site.ImportSideEffectTests.test_system_site_packages)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/work/cpython/cpython-ro-srcdir/Lib/test/test_site.py", line 645, in test_system_site_packages
    self.assertIn(system_paths['purelib'], sys_path)
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: '/home/runner/work/cpython/cpython-builddir/hypovenv/lib/python3.16/site-packages' not found in ['', '/usr/local/lib/python316.zip', '/home/runner/work/cpython/cpython-ro-srcdir/Lib', '/home/runner/work/cpython/cpython-builddir/build/lib.linux-x86_64-3.16', '/tmp/test_python_0lhq3tgr/test.test_site.ImportSideEffectTests.test_system_site_packages-venv-d_ynd3hp/lib/python3.16/site-packages']

@vstinner

vstinner commented Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

I ran a benchmark to compare PyListWriter_Append() to PyList_SetItem() function and to PyList_SET_ITEM() macro. On Free Threading, PyListWriter_Append() is between 2.9x and 3.9x faster than PyList_SetItem() function, and between 1.6x and 1.7x slower than the PyList_SET_ITEM() macro (which is excluded from the limited C API).

PyListWriter_Append() is the function with the highest overhead. PyListWriter_Extend() and PyListWriter_ExtendArray() should have a way lower overhead per item, since they add multiple items at once.

I ran the benchmark on Free Threaded build on Linux (gcc -O3). Benchmark script:

import pyperf, functools, _testcapi
runner = pyperf.Runner()
sizes = (0, 10, 100, 1_000)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_setitem_macro, test_list)
    runner.bench_time_func(f'setitem macro {list_len:,}-list', func)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_setitem_func, test_list)
    runner.bench_time_func(f'setitem func {list_len:,}-list', func)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_writer, test_list)
    runner.bench_time_func(f'writer {list_len:,}-list', func)

Results:

setitem macro 0-list: Mean +- std dev: 12.1 ns +- 0.2 ns
setitem macro 10-list: Mean +- std dev: 33.4 ns +- 1.3 ns
setitem macro 100-list: Mean +- std dev: 219 ns +- 3 ns
setitem macro 1,000-list: Mean +- std dev: 2.21 us +- 0.02 us

setitem func 0-list: Mean +- std dev: 12.3 ns +- 0.1 ns
setitem func 10-list: Mean +- std dev: 155 ns +- 1 ns
setitem func 100-list: Mean +- std dev: 1.47 us +- 0.01 us
setitem func 1,000-list: Mean +- std dev: 14.4 us +- 0.1 us

writer 0-list: Mean +- std dev: 15.0 ns +- 0.1 ns
writer 10-list: Mean +- std dev: 52.8 ns +- 0.2 ns
writer 100-list: Mean +- std dev: 378 ns +- 18 ns
writer 1,000-list: Mean +- std dev: 3.70 us +- 0.04 us
  • "setitem macro" is the fastest way to create a list: PyList_New() + PyList_SET_ITEM() macro -- NOT AVAILABLE in abit3t
  • "setitem func" is the abi3t way, function calls: PyList_New() + PyList_SetItem() function
  • "writer" is this PR: PyListWriter_Create() + PyListWriter_Append() + PyListWriter_Finish()

PyList_SetItem() is between 4.6x and 6.7x slower than the PyList_SET_ITEM() macro: well, that's the issue #158660, PyList_SetItem() function is way slower than PyList_SET_ITEM() macro on Free Threading.

PyListWriter_Append() is between 1.6x and 1.7x slower than the PyList_SET_ITEM() macro, and it's between 2.9x and 3.9x faster than PyList_SetItem() function.

On Free Threading, PyListWriter_Append() is way faster than PyList_SetItem() function because it avoids locking.

Patch adding 3 bench functions to _testcapi:

Details
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index 033ee3234cf..a3a8d9989c0 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -2889,6 +2889,112 @@ uptime_bsd(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
 #endif
 
 
+static PyObject*
+bench_setitem_macro(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyObject *result = PyList_New(len);
+        if (result == NULL) {
+            return NULL;
+        }
+
+        for (Py_ssize_t j=0; j < len; j++) {
+            PyObject *item = Py_NewRef(PyList_GET_ITEM(list, j));
+            PyList_SET_ITEM(result, j, item);
+        }
+
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject*
+bench_setitem_func(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyObject *result = PyList_New(len);
+        if (result == NULL) {
+            return NULL;
+        }
+
+        for (Py_ssize_t j=0; j < len; j++) {
+            PyObject *item = Py_NewRef(PyList_GET_ITEM(list, j));
+            if (PyList_SetItem(result, j, item) < 0) {
+                Py_DECREF(result);
+                return NULL;
+            }
+        }
+
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject*
+bench_writer(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyListWriter *writer = PyListWriter_Create(len);
+        if (writer == NULL) {
+            return NULL;
+        }
+
+        for (Py_ssize_t j=0; j < len; j++) {
+            PyObject *item = PyList_GET_ITEM(list, j);
+            if (PyListWriter_Append(writer, item) < 0) {
+                PyListWriter_Discard(writer);
+                return NULL;
+            }
+        }
+
+        PyObject *result = PyListWriter_Finish(writer);
+        if (result == NULL) {
+            return NULL;
+        }
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
 static PyMethodDef TestMethods[] = {
     {"set_errno",               set_errno,                       METH_VARARGS},
     {"test_config",             test_config,                     METH_NOARGS},
@@ -2988,6 +3094,9 @@ static PyMethodDef TestMethods[] = {
 #ifdef HAVE_SYSCTLBYNAME
     {"uptime_bsd", uptime_bsd, METH_NOARGS},
 #endif
+    {"bench_setitem_macro", bench_setitem_macro, METH_VARARGS},
+    {"bench_setitem_func", bench_setitem_func, METH_VARARGS},
+    {"bench_writer", bench_writer, METH_VARARGS},
     {NULL, NULL} /* sentinel */
 };
 

@vstinner

Copy link
Copy Markdown
Member Author

I also ran benchmarks on PyListWriter_Extend() and PyListWriter_ExtendArray() functions.

setitem macro 0-list: Mean +- std dev: 15.4 ns +- 0.2 ns
setitem macro 10-list: Mean +- std dev: 43.5 ns +- 2.2 ns
setitem macro 100-list: Mean +- std dev: 243 ns +- 4 ns
setitem macro 1,000-list: Mean +- std dev: 2.86 us +- 0.15 us

writer append 0-list: Mean +- std dev: 15.9 ns +- 1.2 ns
writer append 10-list: Mean +- std dev: 53.0 ns +- 0.7 ns
writer append 100-list: Mean +- std dev: 394 ns +- 18 ns
writer append 1,000-list: Mean +- std dev: 3.52 us +- 0.26 us

writer extend 0-list: Mean +- std dev: 29.5 ns +- 1.1 ns
writer extend 10-list: Mean +- std dev: 55.1 ns +- 0.8 ns
writer extend 100-list: Mean +- std dev: 174 ns +- 18 ns
writer extend 1,000-list: Mean +- std dev: 1.50 us +- 0.15 us

writer extend array 0-list: Mean +- std dev: 21.0 ns +- 1.0 ns
writer extend array 10-list: Mean +- std dev: 34.0 ns +- 0.7 ns
writer extend array 100-list: Mean +- std dev: 165 ns +- 11 ns
writer extend array 1,000-list: Mean +- std dev: 1.43 us +- 0.10 us

PyListWriter_Extend() is between 1.3x slower and... 1.9x faster than PyList_SET_ITEM() macro! It's between 1.04x and 2.3x faster than PyListWriter_Append().

PyListWriter_ExtendArray() is between 1.3x and 2.0x faster than PyList_SET_ITEM() macro! It's between 1.6x and 2.6x faster than PyListWriter_Append().

Well, it's not surprising that PyListWriter_Extend() and PyListWriter_ExtendArray() are faster than the PyList_SET_ITEM() macro, since the process many items in a single function call.

Benchmark script:

Details
import pyperf, functools, _testcapi
runner = pyperf.Runner()
sizes = (0, 10, 100, 1_000)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_setitem_macro, test_list)
    runner.bench_time_func(f'setitem macro {list_len:,}-list', func)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_writer_append, test_list)
    runner.bench_time_func(f'writer append {list_len:,}-list', func)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_writer_extend, test_list)
    runner.bench_time_func(f'writer extend {list_len:,}-list', func)

for list_len in sizes:
    test_list = list(range(list_len))
    func = functools.partial(_testcapi.bench_writer_extend_array, test_list)
    runner.bench_time_func(f'writer extend array {list_len:,}-list', func)

Patch adding C benchmarks:

Details
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index 033ee3234cf..c191d12c862 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -2889,6 +2889,185 @@ uptime_bsd(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
 #endif
 
 
+static PyObject*
+bench_setitem_macro(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyObject *result = PyList_New(len);
+        if (result == NULL) {
+            return NULL;
+        }
+
+        for (Py_ssize_t j=0; j < len; j++) {
+            PyObject *item = Py_NewRef(PyList_GET_ITEM(list, j));
+            PyList_SET_ITEM(result, j, item);
+        }
+
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject*
+bench_setitem_func(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyObject *result = PyList_New(len);
+        if (result == NULL) {
+            return NULL;
+        }
+
+        for (Py_ssize_t j=0; j < len; j++) {
+            PyObject *item = Py_NewRef(PyList_GET_ITEM(list, j));
+            if (PyList_SetItem(result, j, item) < 0) {
+                Py_DECREF(result);
+                return NULL;
+            }
+        }
+
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject*
+bench_writer_append(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyListWriter *writer = PyListWriter_Create(len);
+        if (writer == NULL) {
+            return NULL;
+        }
+
+        for (Py_ssize_t j=0; j < len; j++) {
+            PyObject *item = PyList_GET_ITEM(list, j);
+            if (PyListWriter_Append(writer, item) < 0) {
+                PyListWriter_Discard(writer);
+                return NULL;
+            }
+        }
+
+        PyObject *result = PyListWriter_Finish(writer);
+        if (result == NULL) {
+            return NULL;
+        }
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject*
+bench_writer_extend(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyListWriter *writer = PyListWriter_Create(len);
+        if (writer == NULL) {
+            return NULL;
+        }
+
+        if (PyListWriter_Extend(writer, list) < 0) {
+            PyListWriter_Discard(writer);
+            return NULL;
+        }
+
+        PyObject *result = PyListWriter_Finish(writer);
+        if (result == NULL) {
+            return NULL;
+        }
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject*
+bench_writer_extend_array(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    PyObject *list;
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "On", &list, &loops)) {
+        return NULL;
+    }
+
+    PyObject **list_items = _PyList_CAST(list)->ob_item;
+    Py_ssize_t len = PyList_GET_SIZE(list);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    for (Py_ssize_t i=0; i < loops; i++) {
+        PyListWriter *writer = PyListWriter_Create(len);
+        if (writer == NULL) {
+            return NULL;
+        }
+
+        if (PyListWriter_ExtendArray(writer, list_items, len) < 0) {
+            PyListWriter_Discard(writer);
+            return NULL;
+        }
+
+        PyObject *result = PyListWriter_Finish(writer);
+        if (result == NULL) {
+            return NULL;
+        }
+        Py_DECREF(result);
+    }
+    (void)PyTime_PerfCounterRaw(&t2);
+
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
 static PyMethodDef TestMethods[] = {
     {"set_errno",               set_errno,                       METH_VARARGS},
     {"test_config",             test_config,                     METH_NOARGS},
@@ -2988,6 +3167,11 @@ static PyMethodDef TestMethods[] = {
 #ifdef HAVE_SYSCTLBYNAME
     {"uptime_bsd", uptime_bsd, METH_NOARGS},
 #endif
+    {"bench_setitem_macro", bench_setitem_macro, METH_VARARGS},
+    {"bench_setitem_func", bench_setitem_func, METH_VARARGS},
+    {"bench_writer_append", bench_writer_append, METH_VARARGS},
+    {"bench_writer_extend", bench_writer_extend, METH_VARARGS},
+    {"bench_writer_extend_array", bench_writer_extend_array, METH_VARARGS},
     {NULL, NULL} /* sentinel */
 };
 

Comment thread Doc/c-api/list.rst
Extend the list writer with the contents of *items*.

On success, return ``0``.
On error, set an exception and return ``-1``.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should say what happens to the refcounts of items on error (also on success: does it steal references?).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It behaves as PyTuple_FromArray(): new references are created. I will reuse PyTuple_FromArray() phrasing: "copy references from array to the new tuple".

Comment thread Doc/c-api/list.rst
@pitrou

pitrou commented Oct 22, 2026

Copy link
Copy Markdown
Member

+1 on the principle, this looks like a very useful addition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants