https://github.com/python/cpython/commit/6d3127070296d35f292b6f856825cdbf72a18b7c
commit: 6d3127070296d35f292b6f856825cdbf72a18b7c
branch: main
author: Cody Maloney <[email protected]>
committer: cmaloney <[email protected]>
date: 2026-11-03T10:42:33-08:00
summary:

gh-87613: AC VC add support for *args and use it for zip (#160944)

`@vectorcall` accepted `*args`, but the generated code expected an
args tuple. Use fastcall form to parse the args array.

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 <[email protected]>
Co-authored-by: Stan Ulbrych <[email protected]>

files:
M Lib/test/clinic.test.c
M Python/bltinmodule.c
M Python/clinic/bltinmodule.c.h
M Tools/clinic/libclinic/parse_args.py

diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c
index 86b8f81776dfc3e..3b59db65f23b6f6 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/Python/bltinmodule.c b/Python/bltinmodule.c
index 5413e57754667fa..b04715ef30b58cc 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -3139,41 +3139,45 @@ 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);
 
     /* obtain iterators */
-    ittuple = PyTuple_New(tuplesize);
+    ittuple = PyTuple_New(iterables_length);
     if (ittuple == NULL)
         return NULL;
-    for (i=0; i < tuplesize; ++i) {
-        PyObject *item = PyTuple_GET_ITEM(args, i);
-        PyObject *it = PyObject_GetIter(item);
+    for (i=0; i < iterables_length; ++i) {
+        PyObject *it = PyObject_GetIter(iterables[i]);
         if (it == NULL) {
             Py_DECREF(ittuple);
             return NULL;
@@ -3182,12 +3186,12 @@ zip_new(PyTypeObject *type, PyObject *args, PyObject 
*kwds)
     }
 
     /* 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));
     }
 
@@ -3199,7 +3203,7 @@ zip_new(PyTypeObject *type, PyObject *args, PyObject 
*kwds)
         return NULL;
     }
     lz->ittuple = ittuple;
-    lz->tuplesize = tuplesize;
+    lz->tuplesize = iterables_length;
     lz->result = result;
     lz->strict = strict;
 
@@ -3345,21 +3349,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 +3372,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 +3391,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 bdb3dd560b39b8b..09ae184445f7d5d 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]*/
diff --git a/Tools/clinic/libclinic/parse_args.py 
b/Tools/clinic/libclinic/parse_args.py
index 703fa4e94ad5bfa..0d62e230b4a56fe 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:

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to