Uh oh!
There was an error while loading. Please reload this page.
gh-139772: Add PyDict_FromItems() function - #139963
Conversation
vstinner
commented
Oct 11, 2025
For consuming from PyO3 / Rust, I can see this function being obviously useful for cases of small dictionaries with statically known keys (think producing things that look like I think for arbitrary-sized collections, it's probably the case (in Rust) that either:
|
vstinner
commented
Oct 11, 2025
Do you mean producing an array of |
vstinner
commented
Oct 11, 2025
Adding this function would avoid having to make the private PyObject*_PyStack_AsDict(PyObject*const*values, PyObject*kwnames)
{
Py_ssize_tnkwargs;
assert(kwnames!=NULL);
nkwargs=PyTuple_GET_SIZE(kwnames);
return_PyDict_FromItems(&PyTuple_GET_ITEM(kwnames, 0), 1,
values, 1, nkwargs);
} |
davidhewitt
commented
Oct 11, 2025
I was thinking more like 2-tuples, the type might be written in Rust as The 2-tuples are quite a natural structure for Rust producers of "items" (it's what they would expect when iterating a mapping type, for example). But maybe the more common case would be the second one I suggest - a rust iterator producing item 2-tuples with a size hint. At the moment we just start from I could of course use the |
Or name the function in this PR Note that the current private The case of building small literal dicts could also use a
That's really not much different from |
Uh oh!
There was an error while loading. Please reload this page.
davidhewitt
commented
Oct 12, 2025
I think it's ok, the individual tuple items are pointers and so will be aligned appropriately. AFAIK Rust is allowed to reorder tuples to improve packing but guarantees all elements are properly aligned for their type.
True, just that we try not to use private APIs at all in PyO3 so having a public API for this would open up the possibility to use it in PyO3. I understand there's a question about what to do about the unicode optimization with the "presized" API, I suggest we just make it roughly match whatever a normal Python dict would do if created empty and then had items repeatedly added (with the exception that the storage is preallocated). |
vstinner
commented
Oct 12, 2025
So you would prefer this API? PyObject*PyDict_FromItems(PyObject*const*keys, Py_ssize_tkeys_offset,
PyObject*const*values, Py_ssize_tvalues_offset, Py_ssize_tlength) |
vstinner
commented
Oct 12, 2025
In short, you would prefer #139773 API? |
davidhewitt
commented
Oct 16, 2025
I think so, yes (will comment on that thread). |
Benchmark comparing:
UPDATE: Benchmark regenerated to fix a refleak in the Detailsdiff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index c14f925b4e7..55c50eb1605 100644
--- a/Modules/_testcapimodule.c+++ b/Modules/_testcapimodule.c@@ -2595,6 +2595,85 @@ create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args))
}
+static PyObject *+bench_dict_new(PyObject *ob, PyObject *args)+{+ Py_ssize_t size, loops;+ if (!PyArg_ParseTuple(args, "nn", &size, &loops)) {+ return NULL;+ }++ PyTime_t t1, t2;+ PyTime_PerfCounterRaw(&t1);+ for (Py_ssize_t loop=0; loop < loops; loop++) {+ PyObject *d = PyDict_New();+ if (d == NULL) {+ return NULL;+ }++ for (Py_ssize_t i=0; i < size; i++) {+ PyObject *key = PyUnicode_FromFormat("%zi", i);+ assert(key != NULL);++ PyObject *value = PyLong_FromLong(i);+ assert(value != NULL);++ assert(PyDict_SetItem(d, key, value) == 0);+ Py_DECREF(key);+ Py_DECREF(value);+ }++ assert(PyDict_Size(d) == size);+ Py_DECREF(d);+ }+ PyTime_PerfCounterRaw(&t2);++ return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));+}+++static PyObject *+bench_dict_fromitems(PyObject *ob, PyObject *args)+{+ Py_ssize_t size, loops;+ if (!PyArg_ParseTuple(args, "nn", &size, &loops)) {+ return NULL;+ }++ PyTime_t t1, t2;+ PyTime_PerfCounterRaw(&t1);+ for (Py_ssize_t loop=0; loop < loops; loop++) {+ PyObject **items = (PyObject **)PyMem_Malloc(size * 2 * sizeof(PyObject*));+ if (items == NULL) {+ return NULL;+ }++ for (Py_ssize_t i=0; i < size; i++) {+ PyObject *key = PyUnicode_FromFormat("%zi", i);+ assert(key != NULL);++ PyObject *value = PyLong_FromLong(i);+ assert(value != NULL);++ items[i * 2 ] = key;+ items[i * 2 + 1] = value;+ }++ PyObject *d = PyDict_FromItems(items, 2, items + 1, 2, size);+ assert(d != NULL);+ Py_DECREF(d);++ for (Py_ssize_t i=0; i < size * 2; i++) {+ Py_DECREF(items[i]);+ }+ PyMem_Free(items);+ }+ 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},
@@ -2691,6 +2770,8 @@ static PyMethodDef TestMethods[] = {
{"toggle_reftrace_printer", toggle_reftrace_printer, METH_O},
{"create_managed_weakref_nogc_type",
create_managed_weakref_nogc_type, METH_NOARGS},
+ {"bench_dict_new", bench_dict_new, METH_VARARGS},+ {"bench_dict_fromitems", bench_dict_fromitems, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
|
scoder
commented
Oct 19, 2025
Regarding the benchmark numbers, internal loops are obviously faster than a large series of repeated API calls, but I doubt that a |
encukou
commented
Nov 11, 2025
What about a |
vstinner
commented
Nov 13, 2025
|
65bf623 to
9d33600Comparevstinner
commented
Nov 17, 2025
I wrote #141682 to add |
Benchmark on Free Threaded build:
UPDATE: I re-run the benchmark. |
This PR is stale because it has been open for 30 days with no activity. |
vstinner
commented
Jun 9, 2026
I close the PR since I closed the issue: #139772 (comment). |
📚 Documentation preview 📚: https://cpython-previews--139963.org.readthedocs.build/