From bd0660629bc7470a3c89f3080da1bd0e873e990e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 05:57:07 -0400 Subject: [PATCH 01/10] Implement ctypes.util.wrap_dll_function(). --- Lib/ctypes/util.py | 23 +++++++++++++++++++++++ Lib/test/test_ctypes/test_funcptr.py | 21 +++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 51c0d441bc8df0..31ccbca8944a90 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -574,6 +574,29 @@ def inner(decorated_class): return process_the_struct(class_or_none) + +def wrap_dll_function(dll): + def decorator(func): + name = func.__name__ + ptr = getattr(dll, name) + annotations = annotationlib.get_annotations(func) + + try: + restype = annotations.pop("return") + except KeyError as error: + raise ValueError(f"{name} missing return type annotation") from error + + ptr.restype = restype + + argtypes = [] + for argtype in annotations.values(): + argtypes.append(argtype) + + ptr.argtypes = argtypes + return ptr + + return decorator + ################################################################ # test code diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index be641da30eadae..37d19b464c4669 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -2,6 +2,7 @@ import unittest from ctypes import (CDLL, Structure, CFUNCTYPE, sizeof, _CFuncPtr, c_void_p, c_char_p, c_char, c_int, c_uint, c_long) +from ctypes.util import wrap_dll_function from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") from ._support import (_CData, PyCFuncPtrType, Py_TPFLAGS_DISALLOW_INSTANTIATION, @@ -130,6 +131,26 @@ def c_string(init): def test_abstract(self): self.assertRaises(TypeError, _CFuncPtr, 13, "name", 42, "iid") + def test_wrap_dll_function(self): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object) -> ctypes.py_object: + ... + + class Foo: + a = "abc" + + self.assertEqual(PyObject_GetAttr(Foo, "a"), "abc") + + with self.assertRaises(AttributeError): + @wrap_dll_function(ctypes.pythonapi) + def noexist(): + ... + + with self.assertRaises(ValueError): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p): + ... + if __name__ == '__main__': unittest.main() From 428907a9be82a3b939c1f81cb022d71d8a13ce4d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 06:09:28 -0400 Subject: [PATCH 02/10] Add docs. --- Doc/library/ctypes.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 80868a8b2418f3..02e19021e424df 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -694,6 +694,45 @@ through the :attr:`~_CFuncPtr.errcheck` attribute; see the reference manual for details. +Specifying function pointers using type annotations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. function:: wrap_dll_function(dll) + :module: ctypes.util + + A :term:`decorator` that generates :attr:`~_CFuncPtr.argtypes` and + :attr:`~_CFuncPtr.restype` from a function signature, using the + :attr:`~function.__name__` of the function and its :term:`type annotations `. + + The decorated function should look like this:: + + @wrap_dll_function(dll_to_wrap) + def function_ptr_name(arg_name: ctypes_type, ...) -> ctypes_type: + ... + + The body of the decorated function is ignored, and any parameters that are + missing type annotations are skipped. The names of the parameters are ignored + 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. + + For example:: + + import ctypes + from ctypes.util import wrap_dll_function + + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p) -> ctypes.py_object: + ... # Leave this blank + + PyObject_GetAttrString(42, b"real") + + + .. versionadded:: next + + .. _ctypes-passing-pointers: Passing pointers (or: passing parameters by reference) From fbef8190d216aa8e1cb1c96e18bf91d1b2067eb7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 06:17:07 -0400 Subject: [PATCH 03/10] Add blurb and whatsnew entry. --- Doc/library/ctypes.rst | 2 +- Doc/whatsnew/3.16.rst | 3 +++ .../Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 02e19021e424df..23d3eb077658a4 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -697,7 +697,7 @@ see the reference manual for details. Specifying function pointers using type annotations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. function:: wrap_dll_function(dll) +.. decorator:: wrap_dll_function(dll) :module: ctypes.util A :term:`decorator` that generates :attr:`~_CFuncPtr.argtypes` and diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 5cb1a86dac4623..e6565b186a598c 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -237,6 +237,9 @@ ctypes from an annotation-based syntax, similar to how the :mod:`dataclasses` module is used. (Contributed by Peter Bierma in :gh:`104533`.) +* Add :func:`ctypes.util.wrap_dll_function` for generating function pointers + through a function signature. + (Contributed by Peter Bierma in :gh:`153903`.) encodings diff --git a/Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst b/Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst new file mode 100644 index 00000000000000..00f89b471175f4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst @@ -0,0 +1,2 @@ +Add :func:`ctypes.util.wrap_dll_function` for creating +:class:`~ctypes._CFuncPtr` objects from a function signature. From a60fbfa91e961df20f92e3bc394c98f42b49cf55 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:01:04 -0400 Subject: [PATCH 04/10] Fix docs warnings. --- Doc/library/ctypes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 23d3eb077658a4..cc770c2b75bece 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -700,8 +700,8 @@ Specifying function pointers using type annotations .. decorator:: wrap_dll_function(dll) :module: ctypes.util - A :term:`decorator` that generates :attr:`~_CFuncPtr.argtypes` and - :attr:`~_CFuncPtr.restype` from a function signature, using the + A :term:`decorator` that generates :attr:`~ctypes._CFuncPtr.argtypes` and + :attr:`~ctypes._CFuncPtr.restype` from a function signature, using the :attr:`~function.__name__` of the function and its :term:`type annotations `. The decorated function should look like this:: From 0fd0fe469f063c19776fe19ffafb92780b564d4e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:20:00 -0400 Subject: [PATCH 05/10] Update Lib/ctypes/util.py Co-authored-by: Tomas R. --- Lib/ctypes/util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 31ccbca8944a90..b0b9b562cf91bf 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -588,11 +588,7 @@ def decorator(func): ptr.restype = restype - argtypes = [] - for argtype in annotations.values(): - argtypes.append(argtype) - - ptr.argtypes = argtypes + ptr.argtypes = list(annotations.values()) return ptr return decorator From 3a488cdaf80759f2471b47b9b6d03103edbf595e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:20:57 -0400 Subject: [PATCH 06/10] Use "pass" instead of "..." to denote emptiness. --- Doc/library/ctypes.rst | 5 +++-- Lib/test/test_ctypes/test_funcptr.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index cc770c2b75bece..34c8636abaa925 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -708,7 +708,8 @@ Specifying function pointers using type annotations @wrap_dll_function(dll_to_wrap) def function_ptr_name(arg_name: ctypes_type, ...) -> ctypes_type: - ... + # There should be no body + pass The body of the decorated function is ignored, and any parameters that are missing type annotations are skipped. The names of the parameters are ignored @@ -725,7 +726,7 @@ Specifying function pointers using type annotations @wrap_dll_function(ctypes.pythonapi) def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p) -> ctypes.py_object: - ... # Leave this blank + pass PyObject_GetAttrString(42, b"real") diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index 37d19b464c4669..f308c2bf4f2c5a 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -134,7 +134,7 @@ def test_abstract(self): def test_wrap_dll_function(self): @wrap_dll_function(ctypes.pythonapi) def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object) -> ctypes.py_object: - ... + pass class Foo: a = "abc" @@ -144,12 +144,12 @@ class Foo: with self.assertRaises(AttributeError): @wrap_dll_function(ctypes.pythonapi) def noexist(): - ... + pass with self.assertRaises(ValueError): @wrap_dll_function(ctypes.pythonapi) def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p): - ... + pass if __name__ == '__main__': From fcdddea2711f9884cd15513dc9f4cd667163ab51 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:22:17 -0400 Subject: [PATCH 07/10] Assert the exact error message. --- Lib/test/test_ctypes/test_funcptr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index f308c2bf4f2c5a..8cc592daededf1 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -146,7 +146,7 @@ class Foo: def noexist(): pass - with self.assertRaises(ValueError): + with self.assertRaisesRegex(ValueError, "PyObject_GetAttrString missing return type annotation"): @wrap_dll_function(ctypes.pythonapi) def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p): pass From e962991e14797d0f3095acb0d290221e73647367 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:23:56 -0400 Subject: [PATCH 08/10] Use a tuple instead of a list. --- Lib/ctypes/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index b0b9b562cf91bf..6037c2490532d5 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -587,8 +587,7 @@ def decorator(func): raise ValueError(f"{name} missing return type annotation") from error ptr.restype = restype - - ptr.argtypes = list(annotations.values()) + ptr.argtypes = tuple(annotations.values()) return ptr return decorator From 57a1cbf9a2c3d7a5d8a55236945c6f38828c894d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:27:56 -0400 Subject: [PATCH 09/10] Update Lib/ctypes/util.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartosz Sławecki --- Lib/ctypes/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 6037c2490532d5..b035bfb99feee0 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -584,7 +584,7 @@ def decorator(func): try: restype = annotations.pop("return") except KeyError as error: - raise ValueError(f"{name} missing return type annotation") from error + raise ValueError(f"{name!r} missing return type annotation") from error ptr.restype = restype ptr.argtypes = tuple(annotations.values()) From 132927e29e6e915c4cea11b49fe19c1984c3d278 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 07:28:23 -0400 Subject: [PATCH 10/10] Fix test. --- Lib/test/test_ctypes/test_funcptr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index 8cc592daededf1..94d03ad553d222 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -146,7 +146,7 @@ class Foo: def noexist(): pass - with self.assertRaisesRegex(ValueError, "PyObject_GetAttrString missing return type annotation"): + with self.assertRaisesRegex(ValueError, "'PyObject_GetAttrString' missing return type annotation"): @wrap_dll_function(ctypes.pythonapi) def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p): pass