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]