https://github.com/python/cpython/commit/4b9a1caa32dd41f4ba2699bd071fcfe0659db5ab
commit: 4b9a1caa32dd41f4ba2699bd071fcfe0659db5ab
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-22T08:04:50Z
summary:

gh-156219: Fix Argument Clinic for an optional argument with **kwds (GH-156220)

The generated code read an optional positional argument even if it was
not passed.

files:
A Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst
M Lib/test/test_clinic.py
M Modules/_testclinic.c
M Modules/clinic/_testclinic_kwds.c.h
M Tools/clinic/libclinic/parse_args.py

diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index d3e4454348d023..43a1a52874e019 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -4981,6 +4981,18 @@ def test_kwds_with_pos_only(self):
         self.assertEqual(ac_tester.kwds_with_pos_only(1, 2, y='y', z='z'), (1, 
2, kwds))
         self.assertEqual(ac_tester.kwds_with_pos_only(1, 2, **kwds), (1, 2, 
kwds))
 
+    def test_kwds_with_optional_pos_only(self):
+        with self.assertRaises(TypeError):
+            ac_tester.kwds_with_optional_pos_only()
+        with self.assertRaises(TypeError):
+            ac_tester.kwds_with_optional_pos_only(y='y')
+        self.assertEqual(ac_tester.kwds_with_optional_pos_only(1), (1, None, 
{}))
+        self.assertEqual(ac_tester.kwds_with_optional_pos_only(1, 2), (1, 2, 
{}))
+        self.assertEqual(ac_tester.kwds_with_optional_pos_only(1, y='y'),
+                         (1, None, {'y': 'y'}))
+        self.assertEqual(ac_tester.kwds_with_optional_pos_only(1, 2, y='y'),
+                         (1, 2, {'y': 'y'}))
+
     def test_kwds_with_stararg(self):
         self.assertEqual(ac_tester.kwds_with_stararg(), ((), {}))
         self.assertEqual(ac_tester.kwds_with_stararg(1, 2), ((1, 2), {}))
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst
new file mode 100644
index 00000000000000..2b1002da5d0cec
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-22-11-00-00.gh-issue-156219.Wq2nRt.rst
@@ -0,0 +1,2 @@
+Fix Argument Clinic generating code which reads an optional positional
+argument which was not passed, if the function has a ``**kwds`` parameter.
diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c
index ad4e34f640e530..95209cf8127041 100644
--- a/Modules/_testclinic.c
+++ b/Modules/_testclinic.c
@@ -2474,6 +2474,23 @@ kwds_with_pos_only_impl(PyObject *module, PyObject *a, 
PyObject *b,
 }
 
 
+/*[clinic input]
+kwds_with_optional_pos_only
+    a: object
+    b: object = None
+    /
+    **kwds: dict
+[clinic start generated code]*/
+
+static PyObject *
+kwds_with_optional_pos_only_impl(PyObject *module, PyObject *a, PyObject *b,
+                                 PyObject *kwds)
+/*[clinic end generated code: output=25a8458f5acc1a07 input=0b18b9e1670904ec]*/
+{
+    return pack_arguments_newref(3, a, b, kwds);
+}
+
+
 /*[clinic input]
 kwds_with_stararg
     *args: tuple
@@ -2611,6 +2628,7 @@ static PyMethodDef tester_methods[] = {
 
     LONE_KWDS_METHODDEF
     KWDS_WITH_POS_ONLY_METHODDEF
+    KWDS_WITH_OPTIONAL_POS_ONLY_METHODDEF
     KWDS_WITH_STARARG_METHODDEF
     KWDS_WITH_POS_ONLY_AND_STARARG_METHODDEF
 
diff --git a/Modules/clinic/_testclinic_kwds.c.h 
b/Modules/clinic/_testclinic_kwds.c.h
index 86cad50c56cf55..475bb12120c8f8 100644
--- a/Modules/clinic/_testclinic_kwds.c.h
+++ b/Modules/clinic/_testclinic_kwds.c.h
@@ -92,6 +92,53 @@ kwds_with_pos_only(PyObject *module, PyObject *args, 
PyObject *kwargs)
     return return_value;
 }
 
+PyDoc_STRVAR(kwds_with_optional_pos_only__doc__,
+"kwds_with_optional_pos_only($module, a, b=None, /, **kwds)\n"
+"--\n"
+"\n");
+
+#define KWDS_WITH_OPTIONAL_POS_ONLY_METHODDEF    \
+    {"kwds_with_optional_pos_only", 
_PyCFunction_CAST(kwds_with_optional_pos_only), METH_VARARGS|METH_KEYWORDS, 
kwds_with_optional_pos_only__doc__},
+
+static PyObject *
+kwds_with_optional_pos_only_impl(PyObject *module, PyObject *a, PyObject *b,
+                                 PyObject *kwds);
+
+static PyObject *
+kwds_with_optional_pos_only(PyObject *module, PyObject *args, PyObject *kwargs)
+{
+    PyObject *return_value = NULL;
+    PyObject *a;
+    PyObject *b = Py_None;
+    PyObject *__clinic_kwds = NULL;
+
+    if (!_PyArg_CheckPositional("kwds_with_optional_pos_only", 
PyTuple_GET_SIZE(args), 1, 2)) {
+        goto exit;
+    }
+    a = PyTuple_GET_ITEM(args, 0);
+    if (PyTuple_GET_SIZE(args) < 2) {
+        goto skip_optional;
+    }
+    b = PyTuple_GET_ITEM(args, 1);
+skip_optional:
+    if (kwargs == NULL) {
+        __clinic_kwds = PyDict_New();
+        if (__clinic_kwds == NULL) {
+            goto exit;
+        }
+    }
+    else {
+        __clinic_kwds = Py_NewRef(kwargs);
+    }
+    return_value = kwds_with_optional_pos_only_impl(module, a, b, 
__clinic_kwds);
+
+exit:
+    /* Cleanup for kwds */
+    Py_XDECREF(__clinic_kwds);
+
+    return return_value;
+}
+
 PyDoc_STRVAR(kwds_with_stararg__doc__,
 "kwds_with_stararg($module, /, *args, **kwds)\n"
 "--\n"
@@ -181,4 +228,4 @@ kwds_with_pos_only_and_stararg(PyObject *module, PyObject 
*args, PyObject *kwarg
 
     return return_value;
 }
-/*[clinic end generated code: output=3e5251b10aa44382 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=d4e257c529010ae1 input=a9049054013a1b77]*/
diff --git a/Tools/clinic/libclinic/parse_args.py 
b/Tools/clinic/libclinic/parse_args.py
index 37d7cb7ffabe51..f2ea51e3f4e8d1 100644
--- a/Tools/clinic/libclinic/parse_args.py
+++ b/Tools/clinic/libclinic/parse_args.py
@@ -655,6 +655,7 @@ def parse_var_keyword(self) -> None:
                 }}}}
                 """, indent=4))
 
+        has_optional = False
         for i, p in enumerate(self.parameters):
             parse_arg = p.converter.parse_arg(
                 f'PyTuple_GET_ITEM(args, {i})',
@@ -662,7 +663,16 @@ def parse_var_keyword(self) -> None:
                 limited_capi=self.limited_capi,
             )
             assert parse_arg is not None
+            if has_optional or p.is_optional():
+                has_optional = True
+                parser_code.append(libclinic.normalize_snippet("""
+                    if (%s < %d) {{
+                        goto skip_optional;
+                    }}
+                    """, indent=4) % (nargs, i + 1))
             parser_code.append(libclinic.normalize_snippet(parse_arg, 
indent=4))
+        if has_optional:
+            parser_code.append("skip_optional:")
 
         if self.varpos:
             
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))

_______________________________________________
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