https://github.com/python/cpython/commit/b3ddde433e69166ecdf40095349f69e08997e9cb
commit: b3ddde433e69166ecdf40095349f69e08997e9cb
branch: main
author: Vyron Vasileiadis <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-08-30T10:47:53-04:00
summary:

gh-156500: Reject non-positional parameters in `ctypes.util.wrap_dll_function` 
(GH-156501)

argtypes was built from every annotated parameter, so an annotated
keyword-only, *args, or **kwargs parameter contributed an extra positional
entry. Such a parameter has no positional counterpart to describe, so it
now raises ValueError at decoration time.

files:
M Doc/library/ctypes.rst
M Lib/ctypes/util.py
M Lib/test/test_ctypes/test_funcptr.py

diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst
index acd6c2f97348e19..d76c449626fab4e 100644
--- a/Doc/library/ctypes.rst
+++ b/Doc/library/ctypes.rst
@@ -715,8 +715,11 @@ Specifying function pointers using type annotations
    and do not have to match the underlying C implementation.
 
    If the decorated function does not have a return type annotation, a
-   :exc:`ValueError` is raised. If the name of the function does not exist
-   in *dll*, an :exc:`AttributeError` is raised.
+   :exc:`ValueError` is raised. A :exc:`ValueError` is also raised if it has a
+   keyword-only, ``*args``, or ``**kwargs`` parameter, since
+   :attr:`~ctypes._CFuncPtr.argtypes` describes positional arguments only. If
+   the name of the function does not exist in *dll*, an :exc:`AttributeError`
+   is raised.
 
    For example::
 
diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py
index 141d3fbe9382472..2b01506f3d4ffab 100644
--- a/Lib/ctypes/util.py
+++ b/Lib/ctypes/util.py
@@ -4,6 +4,7 @@
 from dataclasses import dataclass
 
 lazy import functools
+lazy import inspect
 lazy import shutil
 lazy import subprocess
 
@@ -509,6 +510,13 @@ def decorator(func):
         except KeyError as error:
             raise ValueError(f"{name!r} missing return type annotation") from 
error
 
+        for param in inspect.signature(func).parameters.values():
+            if param.kind not in (param.POSITIONAL_ONLY,
+                                  param.POSITIONAL_OR_KEYWORD):
+                raise ValueError(f"{name!r} has non-positional parameter "
+                                 f"{param.name!r}; argtypes describes "
+                                 f"positional arguments only")
+
         ptr.restype = restype
         ptr.argtypes = tuple(annotations.values())
         functools.update_wrapper(ptr, func, updated=())
diff --git a/Lib/test/test_ctypes/test_funcptr.py 
b/Lib/test/test_ctypes/test_funcptr.py
index 28ff34f9048454d..b4c27809b5333d5 100644
--- a/Lib/test/test_ctypes/test_funcptr.py
+++ b/Lib/test/test_ctypes/test_funcptr.py
@@ -153,6 +153,47 @@ def noexist():
             def PyObject_GetAttrString(op: ctypes.py_object, attr: 
ctypes.c_char_p):
                 pass
 
+    def test_wrap_dll_function_non_positional(self):
+        # argtypes describes positional arguments only, so a parameter that
+        # cannot be passed positionally is rejected.
+        regex = "'PyObject_GetAttr' has non-positional parameter"
+
+        with self.assertRaisesRegex(ValueError, regex):
+            @wrap_dll_function(ctypes.pythonapi)
+            def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object,
+                                 *args: ctypes.c_int) -> ctypes.py_object:
+                pass
+
+        with self.assertRaisesRegex(ValueError, regex):
+            @wrap_dll_function(ctypes.pythonapi)
+            def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object,
+                                 **kwargs: ctypes.c_int) -> ctypes.py_object:
+                pass
+
+        with self.assertRaisesRegex(ValueError, regex):
+            @wrap_dll_function(ctypes.pythonapi)
+            def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object,
+                                 *, kwonly: ctypes.c_int) -> ctypes.py_object:
+                pass
+
+        with self.assertRaisesRegex(ValueError, regex):
+            @wrap_dll_function(ctypes.pythonapi)
+            def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object,
+                                 *, kwonly) -> ctypes.py_object:
+                pass
+
+        # Positional-only parameters have a positional counterpart, so they
+        # are accepted.
+        @wrap_dll_function(ctypes.pythonapi)
+        def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object,
+                             /) -> ctypes.py_object:
+            pass
+
+        class Foo:
+            a = "abc"
+
+        self.assertEqual(PyObject_GetAttr(Foo, "a"), "abc")
+
     def test_wrap_dll_function_str_ann(self):
         from test.test_ctypes import wrap_str_ann
         version = wrap_str_ann.Py_GetVersion()

_______________________________________________
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