https://github.com/python/cpython/commit/9159287f58f7a5a7e59edffaf3094ea62e1633eb
commit: 9159287f58f7a5a7e59edffaf3094ea62e1633eb
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-03-06T21:57:44Z
summary:

gh-144175: Add PyArg_ParseArray() function (#144283)

Add PyArg_ParseArray() and PyArg_ParseArrayAndKeywords()
functions to parse arguments of functions using the METH_FASTCALL
calling convention.

Co-authored-by: Bénédikt Tran <[email protected]>

files:
A Misc/NEWS.d/next/C_API/2026-01-27-18-15-15.gh-issue-144175.qHK5Jf.rst
M Doc/c-api/arg.rst
M Doc/whatsnew/3.15.rst
M Include/cpython/modsupport.h
M Lib/test/test_capi/test_modsupport.py
M Modules/_testcapi/modsupport.c
M Python/getargs.c

diff --git a/Doc/c-api/arg.rst b/Doc/c-api/arg.rst
index fd6be6a9b67a03..4a3a6347239c4f 100644
--- a/Doc/c-api/arg.rst
+++ b/Doc/c-api/arg.rst
@@ -516,6 +516,28 @@ API Functions
        }
 
 
+.. c:function:: int PyArg_ParseArray(PyObject *const *args, Py_ssize_t nargs, 
const char *format, ...)
+
+   Parse the parameters of a function that takes only array parameters into
+   local variables (that is, a function using the :c:macro:`METH_FASTCALL`
+   calling convention).
+   Returns true on success; on failure, it returns false and raises the
+   appropriate exception.
+
+   .. versionadded:: next
+
+
+.. c:function:: int PyArg_ParseArrayAndKeywords(PyObject *const *args, 
Py_ssize_t nargs, PyObject *kwnames, const char *format, const char * const 
*kwlist, ...)
+
+   Parse the parameters of a function that takes both array and keyword
+   parameters into local variables (that is, a function using the
+   :c:macro:`METH_FASTCALL` ``|`` :c:macro:`METH_KEYWORDS` calling convention).
+   Returns true on success; on failure, it returns false and raises the
+   appropriate exception.
+
+   .. versionadded:: next
+
+
 .. c:function:: int PyArg_UnpackTuple(PyObject *args, const char *name, 
Py_ssize_t min, Py_ssize_t max, ...)
 
    A simpler form of parameter retrieval which does not use a format string to
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 0b5902bb013436..42b6171c1a83a2 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1607,6 +1607,11 @@ C API changes
 New features
 ------------
 
+* Add :c:func:`PyArg_ParseArray` and :c:func:`PyArg_ParseArrayAndKeywords`
+  functions to parse arguments of functions using the :c:macro:`METH_FASTCALL`
+  calling convention.
+  (Contributed by Victor Stinner in :gh:`144175`.)
+
 * Add the following functions for the new :class:`frozendict` type:
 
   * :c:func:`PyAnyDict_Check`
diff --git a/Include/cpython/modsupport.h b/Include/cpython/modsupport.h
index 6134442106474f..b9f253e06b31c9 100644
--- a/Include/cpython/modsupport.h
+++ b/Include/cpython/modsupport.h
@@ -2,6 +2,19 @@
 #  error "this header file must not be included directly"
 #endif
 
+PyAPI_FUNC(int) PyArg_ParseArray(
+    PyObject *const *args,
+    Py_ssize_t nargs,
+    const char *format,
+    ...);
+PyAPI_FUNC(int) PyArg_ParseArrayAndKeywords(
+    PyObject *const *args,
+    Py_ssize_t nargs,
+    PyObject *kwnames,
+    const char *format,
+    const char * const *kwlist,
+    ...);
+
 // A data structure that can be used to run initialization code once in a
 // thread-safe manner. The C++11 equivalent is std::call_once.
 typedef struct {
diff --git a/Lib/test/test_capi/test_modsupport.py 
b/Lib/test/test_capi/test_modsupport.py
index 1520489f843826..29bebf847aaba2 100644
--- a/Lib/test/test_capi/test_modsupport.py
+++ b/Lib/test/test_capi/test_modsupport.py
@@ -152,3 +152,22 @@ def test_negative_freethreading(self, modname, minor, 
build):
             msg = "only compatible with free-threaded CPython"
         with self.assertRaisesRegex(ImportError, msg):
             _testcapi.pyabiinfo_check(modname, 1, minor, ft_flag, build, 0)
+
+
+class TestModsupport(unittest.TestCase):
+    def test_pyarg_parsearray(self):
+        func = _testcapi.pyarg_parsearray
+        self.assertEqual(func(1, 2), (1, 2, 0))
+        self.assertEqual(func(1, 2, 3), (1, 2, 3))
+        self.assertRaises(TypeError, func, 1)
+        self.assertRaises(TypeError, func, "str", 2)
+
+    def test_funcandkeywords(self):
+        func = _testcapi.pyarg_parsearrayandkeywords
+        self.assertEqual(func(1, 2), (1, 2, 0))
+        self.assertEqual(func(1, 2, 3), (1, 2, 3))
+        self.assertEqual(func(1, b=2), (1, 2, 0))
+        self.assertEqual(func(1, b=2, c=3), (1, 2, 3))
+        self.assertRaises(TypeError, func, 1)
+        self.assertRaises(TypeError, func, "str", 2)
+        self.assertRaises(TypeError, func, 1, z=2)
diff --git 
a/Misc/NEWS.d/next/C_API/2026-01-27-18-15-15.gh-issue-144175.qHK5Jf.rst 
b/Misc/NEWS.d/next/C_API/2026-01-27-18-15-15.gh-issue-144175.qHK5Jf.rst
new file mode 100644
index 00000000000000..da1e489bb3d2e5
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-01-27-18-15-15.gh-issue-144175.qHK5Jf.rst
@@ -0,0 +1,3 @@
+Add :c:func:`PyArg_ParseArray` and :c:func:`PyArg_ParseArrayAndKeywords`
+functions to parse arguments of functions using the :c:macro:`METH_FASTCALL`
+calling convention. Patch by Victor Stinner.
diff --git a/Modules/_testcapi/modsupport.c b/Modules/_testcapi/modsupport.c
index 6746eb9eb1e94a..151e4aa19afe11 100644
--- a/Modules/_testcapi/modsupport.c
+++ b/Modules/_testcapi/modsupport.c
@@ -25,8 +25,36 @@ pyabiinfo_check(PyObject *Py_UNUSED(module), PyObject *args)
     Py_RETURN_NONE;
 }
 
+static PyObject *
+pyarg_parsearray(PyObject* self, PyObject* const* args, Py_ssize_t nargs)
+{
+    int a, b, c = 0;
+    if (!PyArg_ParseArray(args, nargs, "ii|i", &a, &b, &c)) {
+        return NULL;
+    }
+    return Py_BuildValue("iii", a, b, c);
+}
+
+static PyObject *
+pyarg_parsearrayandkeywords(PyObject* self, PyObject* const* args,
+                            Py_ssize_t nargs, PyObject* kwnames)
+{
+    int a, b, c = 0;
+    const char *kwlist[] = {"a", "b", "c", NULL};
+    if (!PyArg_ParseArrayAndKeywords(args, nargs, kwnames,
+                                     "ii|i", kwlist,
+                                     &a, &b, &c)) {
+        return NULL;
+    }
+    return Py_BuildValue("iii", a, b, c);
+}
+
 static PyMethodDef TestMethods[] = {
     {"pyabiinfo_check", pyabiinfo_check, METH_VARARGS},
+    {"pyarg_parsearray", _PyCFunction_CAST(pyarg_parsearray), METH_FASTCALL},
+    {"pyarg_parsearrayandkeywords",
+     _PyCFunction_CAST(pyarg_parsearrayandkeywords),
+     METH_FASTCALL | METH_KEYWORDS},
     {NULL},
 };
 
diff --git a/Python/getargs.c b/Python/getargs.c
index c119ca5c35398b..31cd4ad3f652d9 100644
--- a/Python/getargs.c
+++ b/Python/getargs.c
@@ -57,8 +57,15 @@ static const char *convertsimple(PyObject *, const char **, 
va_list *, int,
 static Py_ssize_t convertbuffer(PyObject *, const void **p, const char **);
 static int getbuffer(PyObject *, Py_buffer *, const char**);
 
-static int vgetargskeywords(PyObject *, PyObject *,
-                            const char *, const char * const *, va_list *, 
int);
+static int
+vgetargskeywords(PyObject *args, PyObject *kwargs,
+                 const char *format, const char * const *kwlist,
+                 va_list *p_va, int flags);
+static int
+vgetargskeywords_impl(PyObject *const *args, Py_ssize_t nargs,
+                      PyObject *kwargs, PyObject *kwnames,
+                      const char *format, const char * const *kwlist,
+                      va_list *p_va, int flags);
 static int vgetargskeywordsfast(PyObject *, PyObject *,
                             struct _PyArg_Parser *, va_list *, int);
 static int vgetargskeywordsfast_impl(PyObject *const *args, Py_ssize_t nargs,
@@ -129,6 +136,40 @@ _PyArg_ParseStack(PyObject *const *args, Py_ssize_t nargs, 
const char *format, .
     return retval;
 }
 
+int
+PyArg_ParseArray(PyObject *const *args, Py_ssize_t nargs, const char *format, 
...)
+{
+    va_list va;
+    va_start(va, format);
+    int retval = vgetargs1_impl(NULL, args, nargs, format, &va, 0);
+    va_end(va);
+    return retval;
+}
+
+int
+PyArg_ParseArrayAndKeywords(PyObject *const *args, Py_ssize_t nargs,
+                            PyObject *kwnames,
+                            const char *format,
+                            const char * const *kwlist, ...)
+{
+    if ((args == NULL && nargs != 0) ||
+        (kwnames != NULL && !PyTuple_Check(kwnames)) ||
+        format == NULL ||
+        kwlist == NULL)
+    {
+        PyErr_BadInternalCall();
+        return 0;
+    }
+
+    va_list va;
+    va_start(va, kwlist);
+    int retval = vgetargskeywords_impl(args, nargs, NULL, kwnames, format,
+                                       kwlist, &va, 0);
+    va_end(va);
+    return retval;
+}
+
+
 int
 PyArg_VaParse(PyObject *args, const char *format, va_list va)
 {
@@ -1612,11 +1653,27 @@ PyArg_ValidateKeywordArguments(PyObject *kwargs)
 static PyObject *
 new_kwtuple(const char * const *keywords, int total, int pos);
 
+static PyObject*
+find_keyword_str(PyObject *kwnames, PyObject *const *kwstack, const char *key)
+{
+    Py_ssize_t nkwargs = PyTuple_GET_SIZE(kwnames);
+    for (Py_ssize_t i = 0; i < nkwargs; i++) {
+        PyObject *kwname = PyTuple_GET_ITEM(kwnames, i);
+        assert(PyUnicode_Check(kwname));
+        if (PyUnicode_EqualToUTF8(kwname, key)) {
+            return Py_NewRef(kwstack[i]);
+        }
+    }
+    return NULL;
+}
+
 #define IS_END_OF_FORMAT(c) (c == '\0' || c == ';' || c == ':')
 
 static int
-vgetargskeywords(PyObject *args, PyObject *kwargs, const char *format,
-                 const char * const *kwlist, va_list *p_va, int flags)
+vgetargskeywords_impl(PyObject *const *args, Py_ssize_t nargs,
+                      PyObject *kwargs, PyObject *kwnames,
+                      const char *format, const char * const *kwlist,
+                      va_list *p_va, int flags)
 {
     char msgbuf[512];
     int levels[32];
@@ -1625,16 +1682,18 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, 
const char *format,
     int max = INT_MAX;
     int i, pos, len;
     int skip = 0;
-    Py_ssize_t nargs, nkwargs;
+    Py_ssize_t nkwargs;
     freelistentry_t static_entries[STATIC_FREELIST_ENTRIES];
     freelist_t freelist;
+    PyObject * const *kwstack = NULL;
 
     freelist.entries = static_entries;
     freelist.first_available = 0;
     freelist.entries_malloced = 0;
 
-    assert(args != NULL && PyTuple_Check(args));
+    assert(args != NULL || nargs == 0);
     assert(kwargs == NULL || PyDict_Check(kwargs));
+    assert(kwnames == NULL || PyTuple_Check(kwnames));
     assert(format != NULL);
     assert(kwlist != NULL);
     assert(p_va != NULL);
@@ -1672,8 +1731,16 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const 
char *format,
         freelist.entries_malloced = 1;
     }
 
-    nargs = PyTuple_GET_SIZE(args);
-    nkwargs = (kwargs == NULL) ? 0 : PyDict_GET_SIZE(kwargs);
+    if (kwargs != NULL) {
+        nkwargs = PyDict_GET_SIZE(kwargs);
+    }
+    else if (kwnames != NULL) {
+        nkwargs = PyTuple_GET_SIZE(kwnames);
+        kwstack = args + nargs;
+    }
+    else {
+        nkwargs = 0;
+    }
     if (nargs + nkwargs > len) {
         /* Adding "keyword" (when nargs == 0) prevents producing wrong error
            messages in some special cases (see bpo-31229). */
@@ -1757,11 +1824,16 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, 
const char *format,
         if (!skip) {
             PyObject *current_arg;
             if (i < nargs) {
-                current_arg = Py_NewRef(PyTuple_GET_ITEM(args, i));
+                current_arg = Py_NewRef(args[i]);
             }
             else if (nkwargs && i >= pos) {
-                if (PyDict_GetItemStringRef(kwargs, kwlist[i], &current_arg) < 
0) {
-                    return cleanreturn(0, &freelist);
+                if (kwargs != NULL) {
+                    if (PyDict_GetItemStringRef(kwargs, kwlist[i], 
&current_arg) < 0) {
+                        return cleanreturn(0, &freelist);
+                    }
+                }
+                else {
+                    current_arg = find_keyword_str(kwnames, kwstack, 
kwlist[i]);
                 }
                 if (current_arg) {
                     --nkwargs;
@@ -1846,8 +1918,13 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const 
char *format,
         /* make sure there are no arguments given by name and position */
         for (i = pos; i < nargs; i++) {
             PyObject *current_arg;
-            if (PyDict_GetItemStringRef(kwargs, kwlist[i], &current_arg) < 0) {
-                return cleanreturn(0, &freelist);
+            if (kwargs != NULL) {
+                if (PyDict_GetItemStringRef(kwargs, kwlist[i], &current_arg) < 
0) {
+                    return cleanreturn(0, &freelist);
+                }
+            }
+            else {
+                current_arg = find_keyword_str(kwnames, kwstack, kwlist[i]);
             }
             if (current_arg) {
                 Py_DECREF(current_arg);
@@ -1863,7 +1940,20 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const 
char *format,
         }
         /* make sure there are no extraneous keyword arguments */
         j = 0;
-        while (PyDict_Next(kwargs, &j, &key, NULL)) {
+        while (1) {
+            if (kwargs != NULL) {
+                if (!PyDict_Next(kwargs, &j, &key, NULL)) {
+                    break;
+                }
+            }
+            else {
+                if (j >= nkwargs) {
+                    break;
+                }
+                key = PyTuple_GET_ITEM(kwnames, j);
+                j++;
+            }
+
             int match = 0;
             if (!PyUnicode_Check(key)) {
                 PyErr_SetString(PyExc_TypeError,
@@ -1921,6 +2011,16 @@ vgetargskeywords(PyObject *args, PyObject *kwargs, const 
char *format,
     return cleanreturn(1, &freelist);
 }
 
+static int
+vgetargskeywords(PyObject *argstuple, PyObject *kwargs,
+                 const char *format, const char * const *kwlist,
+                 va_list *p_va, int flags)
+{
+    PyObject *const *args = _PyTuple_ITEMS(argstuple);
+    Py_ssize_t nargs = PyTuple_GET_SIZE(argstuple);
+    return vgetargskeywords_impl(args, nargs, kwargs, NULL,
+                                 format, kwlist, p_va, flags);
+}
 
 static int
 scan_keywords(const char * const *keywords, int *ptotal, int *pposonly)

_______________________________________________
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