From c121f81c2f5b55e6c8ced6262dfc4f993c5ab94c Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 29 Oct 2026 17:54:53 -0700 Subject: [PATCH 1/3] gh-87613: Support *args in Argument Clinic vectorcall `@vectorcall` accepted `*args`, but the generated code expected an args tuple. Use fastcall form to parse the args array. Co-Authored-By: Claude Opus 5.5 --- Lib/test/clinic.test.c | 31 +++++++++++++++++++++++++++- Tools/clinic/libclinic/parse_args.py | 26 ++++++++++++++++------- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c index 86b8f81776dfc3..3b59db65f23b6f 100644 --- a/Lib/test/clinic.test.c +++ b/Lib/test/clinic.test.c @@ -5220,6 +5220,7 @@ Test___init___impl(TestObj *self, PyObject *args) /*[clinic end generated code: output=0e5836c40dbc2397 input=a615a4485c0fc3e2]*/ /*[clinic input] +@vectorcall @classmethod Test.__new__ *args: tuple @@ -5261,9 +5262,37 @@ Test(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +Test_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + PyObject *return_value = NULL; + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + PyObject *__clinic_args = NULL; + + assert(_PyType_CAST(type)->tp_new == Test); + /* Make sure the type object is immutable: the generated + * vectorcall doesn't deal e.g. with users reassigning __init__. */ + assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE)); + if (!_PyArg_NoKwnames("Test", kwnames)) { + goto exit; + } + __clinic_args = PyTuple_FromArray(args, nargs); + if (__clinic_args == NULL) { + goto exit; + } + return_value = Test_impl(_PyType_CAST(type), __clinic_args); + +exit: + /* Cleanup for args */ + Py_XDECREF(__clinic_args); + + return return_value; +} + static PyObject * Test_impl(PyTypeObject *type, PyObject *args) -/*[clinic end generated code: output=e6fba0c8951882fd input=8ce30adb836aeacb]*/ +/*[clinic end generated code: output=07f68e3971ed24cc input=1dc824fd6cec5f8e]*/ /*[clinic input] diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 703fa4e94ad5bf..0d62e230b4a56f 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -923,14 +923,14 @@ def render_option_groups_parse_tuple( parser_code.append(" }}") return parser_code - def _parse_vararg(self) -> str: + def _parse_vararg(self, *, fastcall: bool) -> str: assert self.varpos is not None c = self.varpos.converter assert isinstance(c, libclinic.converters.VarPosCConverter) return c.parse_vararg(pos_only=self.pos_only, min_pos=self.min_pos, max_pos=self.max_pos, - fastcall=self.fastcall, + fastcall=fastcall, limited_capi=self.limited_capi) def _parse_kwarg(self) -> str: @@ -963,6 +963,7 @@ def _parse_positional_args( argname_fmt: str, nargs: str, limited_capi: bool, + fastcall: bool, ) -> list[str] | None: """Emit per-parameter positional argument parsing. @@ -995,8 +996,8 @@ def _parse_positional_args( if self.min_pos < len(self.parameters): parser_code.append("skip_optional:") if self.varpos: - parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), - indent=4)) + parser_code.append(libclinic.normalize_snippet( + self._parse_vararg(fastcall=fastcall), indent=4)) elif self.var_keyword: parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4)) @@ -1090,7 +1091,7 @@ def parse_pos_only(self) -> None: pos_code = self._parse_positional_args( argname_fmt=argname_fmt, nargs=nargs, - limited_capi=self.limited_capi) + limited_capi=self.limited_capi, fastcall=self.fastcall) if pos_code is not None: parser_code.extend(pos_code) else: @@ -1148,7 +1149,8 @@ def parse_var_keyword(self) -> None: parser_code.append("skip_optional:") if self.varpos: - parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4)) + parser_code.append(libclinic.normalize_snippet( + self._parse_vararg(fastcall=self.fastcall), indent=4)) if self.var_keyword: parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4)) self.parser_body(*parser_code) @@ -1376,7 +1378,12 @@ def parse_general(self, clang: CLanguage) -> None: if add_label: parser_code.append("%s:" % add_label) if self.varpos: - parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4)) + # The vectorcall helper gets an array of arguments, not a + # tuple: use the converters' fastcall convention, which + # handles that. + fastcall = True if self.func.vectorcall else self.fastcall + parser_code.append(libclinic.normalize_snippet( + self._parse_vararg(fastcall=fastcall), indent=4)) else: self.use_converters() @@ -1617,8 +1624,11 @@ def _vectorcall_positional(self, *, arity_checked: Already have a number of arguments check. """ + # The vectorcall gets an array of arguments, not a tuple: use the + # converters' fastcall convention, which handles that. pos_code = self._parse_positional_args( - argname_fmt='args[%d]', nargs='nargs', limited_capi=False) + argname_fmt='args[%d]', nargs='nargs', limited_capi=False, + fastcall=True) # Converter support was validated when @vectorcall was parsed. assert pos_code is not None if arity_checked: From 33a487180b0490cb1fb95d8b343fd1d3971b5227 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Thu, 29 Oct 2026 18:12:12 -0700 Subject: [PATCH 2/3] gh-87613: Use Argument Clinic vectorcall for zip zip had no vectorcall, so every call built an args tuple and a kwargs dict, and strict=True also went through PyArg_ParseTupleAndKeywords with a temporary empty tuple. Argument error messages are unchanged except for unknown keywords: `zip(strict=1, x=2)` now reports "got an unexpected keyword argument 'x'" instead of "takes at most 1 keyword argument (2 given)". The docstring gains a summary line; `__text_signature__` is unchanged. pyperf microbenchmark (PGO+LTO, run serially): | Benchmark | base | zip | |------------------------|:-------:|:---------------------:| | zip(a, b, strict=True) | 118 ns | 61.5 ns: 1.92x faster | | zip(a, b) | 64.9 ns | 53.1 ns: 1.22x faster | | zip(a) | 55.0 ns | 47.1 ns: 1.17x faster | | list(zip(a, b)) | 123 ns | 113 ns: 1.09x faster | | zip() | 27.4 ns | 25.7 ns: 1.07x faster | | Sub(a, b) | 71.5 ns | 70.6 ns: 1.01x faster | Benchmark hidden because not significant (1): zip.`__new__` Benchmark code: ```python import pyperf runner = pyperf.Runner() setup = "a = (1, 2, 3); b = [4, 5, 6]" for stmt in ["zip()", "zip(a)", "zip(a, b)", "zip(a, b, strict=True)", "list(zip(a, b))"]: runner.timeit(stmt, stmt, setup) runner.timeit('zip.`__new__`', 'new(zip, a, b)', setup + '; new = zip.__new__') runner.timeit('Sub(a, b)', 'S(a, b)', setup + '\nclass S(zip): pass') ``` Co-Authored-By: Claude Opus 5.5 --- Python/bltinmodule.c | 70 ++++++++++------------ Python/clinic/bltinmodule.c.h | 107 +++++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 40 deletions(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 40471af7186ae3..4f625adfa02419 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -3139,41 +3139,47 @@ typedef struct { #define _zipobject_CAST(op) ((zipobject *)(op)) +/*[clinic input] +class zip "zipobject *" "&PyZip_Type" +@vectorcall +@classmethod +zip.__new__ as zip_new + + *iterables: array + strict: bool = False + +Iterate over several iterables in parallel, yielding tuples. + +The zip object yields n-length tuples, where n is the number of +iterables passed as positional arguments to zip(). The i-th element +in every tuple comes from the i-th iterable argument to zip(). This +continues until the shortest argument is exhausted. + +If strict is true and one of the arguments is exhausted before the +others, raise a ValueError. + + >>> list(zip('abcdefg', range(3), range(4))) + [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)] +[clinic start generated code]*/ + static PyObject * -zip_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +zip_new_impl(PyTypeObject *type, PyObject * const *iterables, + Py_ssize_t iterables_length, int strict) +/*[clinic end generated code: output=5e1a30829ce1f6ab input=4bd8e6a24e4bfb2e]*/ + { zipobject *lz; Py_ssize_t i; PyObject *ittuple; /* tuple of iterators */ PyObject *result; - Py_ssize_t tuplesize; - int strict = 0; - - if (kwds) { - PyObject *empty = PyTuple_New(0); - if (empty == NULL) { - return NULL; - } - static char *kwlist[] = {"strict", NULL}; - int parsed = PyArg_ParseTupleAndKeywords( - empty, kwds, "|$p:zip", kwlist, &strict); - Py_DECREF(empty); - if (!parsed) { - return NULL; - } - } - - /* args must be a tuple */ - assert(PyTuple_Check(args)); - tuplesize = PyTuple_GET_SIZE(args); + Py_ssize_t tuplesize = iterables_length; /* obtain iterators */ ittuple = PyTuple_New(tuplesize); if (ittuple == NULL) return NULL; for (i=0; i < tuplesize; ++i) { - PyObject *item = PyTuple_GET_ITEM(args, i); - PyObject *it = PyObject_GetIter(item); + PyObject *it = PyObject_GetIter(iterables[i]); if (it == NULL) { Py_DECREF(ittuple); return NULL; @@ -3345,21 +3351,6 @@ static PyMethodDef zip_methods[] = { {NULL} /* sentinel */ }; -PyDoc_STRVAR(zip_doc, -"zip(*iterables, strict=False)\n\ ---\n\ -\n\ -The zip object yields n-length tuples, where n is the number of\n\ -iterables passed as positional arguments to zip(). The i-th element\n\ -in every tuple comes from the i-th iterable argument to zip(). This\n\ -continues until the shortest argument is exhausted.\n\ -\n\ -If strict is true and one of the arguments is exhausted before the\n\ -others, raise a ValueError.\n\ -\n\ - >>> list(zip('abcdefg', range(3), range(4)))\n\ - [('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]"); - PyTypeObject PyZip_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "zip", /* tp_name */ @@ -3383,7 +3374,7 @@ PyTypeObject PyZip_Type = { 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE, /* tp_flags */ - zip_doc, /* tp_doc */ + zip_new__doc__, /* tp_doc */ zip_traverse, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ @@ -3402,6 +3393,7 @@ PyTypeObject PyZip_Type = { PyType_GenericAlloc, /* tp_alloc */ zip_new, /* tp_new */ PyObject_GC_Del, /* tp_free */ + .tp_vectorcall = zip_vectorcall, }; diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index bdb3dd560b39b8..09ae184445f7d5 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -1495,4 +1495,109 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=1dacff294aaa7485 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(zip_new__doc__, +"zip(*iterables, strict=False)\n" +"--\n" +"\n" +"Iterate over several iterables in parallel, yielding tuples.\n" +"\n" +"The zip object yields n-length tuples, where n is the number of\n" +"iterables passed as positional arguments to zip(). The i-th element\n" +"in every tuple comes from the i-th iterable argument to zip(). This\n" +"continues until the shortest argument is exhausted.\n" +"\n" +"If strict is true and one of the arguments is exhausted before the\n" +"others, raise a ValueError.\n" +"\n" +" >>> list(zip(\'abcdefg\', range(3), range(4)))\n" +" [(\'a\', 0, 0), (\'b\', 1, 1), (\'c\', 2, 2)]"); + +static PyObject * +zip_new_impl(PyTypeObject *type, PyObject * const *iterables, + Py_ssize_t iterables_length, int strict); + +static PyObject * +zip_new_helper(PyTypeObject *type, PyObject *const *args, + Py_ssize_t nargs, Py_ssize_t nkw, PyObject *kwargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(strict), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"strict", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "zip", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject * const *fastargs; + Py_ssize_t noptargs = 0 + nkw - 0; + PyObject * const *iterables; + Py_ssize_t iterables_length; + int strict = 0; + + fastargs = _PyArg_UnpackKeywords(args, nargs, kwargs, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + strict = PyObject_IsTrue(fastargs[0]); + if (strict < 0) { + goto exit; + } +skip_optional_kwonly: + iterables = args; + iterables_length = nargs; + return_value = zip_new_impl(type, iterables, iterables_length, strict); + +exit: + return return_value; +} + +static PyObject * +zip_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + return zip_new_helper(type, _PyTuple_CAST(args)->ob_item, + PyTuple_GET_SIZE(args), + kwargs ? PyDict_GET_SIZE(kwargs) : 0, + kwargs, NULL); +} + +static PyObject * +zip_vectorcall(PyObject *type, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ + Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); + + assert(_PyType_CAST(type)->tp_new == zip_new); + /* Make sure the type object is immutable: the generated + * vectorcall doesn't deal e.g. with users reassigning __init__. */ + assert(PyType_HasFeature(_PyType_CAST(type), Py_TPFLAGS_IMMUTABLETYPE)); + return zip_new_helper(_PyType_CAST(type), args, nargs, + kwnames ? PyTuple_GET_SIZE(kwnames) : 0, + NULL, kwnames); +} +/*[clinic end generated code: output=ef3ea633d4ed8708 input=a9049054013a1b77]*/ From 607ad5a4c4ffcf6ee02c5de81c2dec19a7e79e07 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Fri, 30 Oct 2026 12:50:13 -0700 Subject: [PATCH 3/3] gh-87613: zip: Drop tuplesize local --- Python/bltinmodule.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 4f625adfa02419..b632acbdf67c5f 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -3172,13 +3172,12 @@ zip_new_impl(PyTypeObject *type, PyObject * const *iterables, Py_ssize_t i; PyObject *ittuple; /* tuple of iterators */ PyObject *result; - Py_ssize_t tuplesize = iterables_length; /* obtain iterators */ - ittuple = PyTuple_New(tuplesize); + ittuple = PyTuple_New(iterables_length); if (ittuple == NULL) return NULL; - for (i=0; i < tuplesize; ++i) { + for (i=0; i < iterables_length; ++i) { PyObject *it = PyObject_GetIter(iterables[i]); if (it == NULL) { Py_DECREF(ittuple); @@ -3188,12 +3187,12 @@ zip_new_impl(PyTypeObject *type, PyObject * const *iterables, } /* create a result holder */ - result = PyTuple_New(tuplesize); + result = PyTuple_New(iterables_length); if (result == NULL) { Py_DECREF(ittuple); return NULL; } - for (i=0 ; i < tuplesize ; i++) { + for (i=0 ; i < iterables_length ; i++) { PyTuple_SET_ITEM(result, i, Py_NewRef(Py_None)); } @@ -3205,7 +3204,7 @@ zip_new_impl(PyTypeObject *type, PyObject * const *iterables, return NULL; } lz->ittuple = ittuple; - lz->tuplesize = tuplesize; + lz->tuplesize = iterables_length; lz->result = result; lz->strict = strict;