diff --git a/Lib/test/test_ctypes/test_callbacks.py b/Lib/test/test_ctypes/test_callbacks.py index c7f287d9fb6293d..8fff5fe4c8e2f30 100644 --- a/Lib/test/test_ctypes/test_callbacks.py +++ b/Lib/test/test_ctypes/test_callbacks.py @@ -559,6 +559,27 @@ def cb(): self.assertEqual(_testfunc_callback_int_to_longlong(cb), -1) + def test_mismatched_callback_type_error_includes_signature(self): + # gh-125788: mismatched callback types used to say + # "expected CFunctionType instance instead of CFunctionType". + Expected = CFUNCTYPE(c_int, c_void_p, c_void_p) + Actual = CFUNCTYPE(c_int, c_int, c_int) + + def callback(a, b): + return 0 + + actual = Actual(callback) + with self.assertRaises(TypeError) as cm: + Expected.from_param(actual) + msg = str(cm.exception) + self.assertIn("signature", msg) + self.assertIn("restype", msg) + self.assertIn("argtypes", msg) + self.assertNotEqual( + msg, + f"expected {Expected.__name__} instance instead of {type(actual).__name__}", + ) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-10-31-14-46-04.gh-issue-125788.dEn5yH.rst b/Misc/NEWS.d/next/Library/2026-10-31-14-46-04.gh-issue-125788.dEn5yH.rst new file mode 100644 index 000000000000000..7f4d61ac32aa1ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-31-14-46-04.gh-issue-125788.dEn5yH.rst @@ -0,0 +1,4 @@ +:mod:`ctypes` :exc:`TypeError` messages for mismatched callback function +types now include the expected and actual signatures (``restype`` and +``argtypes``). Previously both types often appeared only as +``CFunctionType`` or ``WinFunctionType``. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 1035c91005759df..dff32789342c8ca 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1123,6 +1123,32 @@ CDataType_from_param_impl(PyObject *type, PyTypeObject *cls, PyObject *value) _Py_LeaveRecursiveCall(); return value; } + /* Callback function pointer types often share the same tp_name + ("CFunctionType" / "WinFunctionType"), so include signatures when + both the expected type and the value are callback types (#125788). */ + if (PyCFuncPtrTypeObject_Check(st, type) && + PyCFuncPtrObject_Check(st, value)) { + StgInfo *expected_info; + StgInfo *actual_info; + if (PyStgInfo_FromType(st, type, &expected_info) < 0) { + return NULL; + } + if (PyStgInfo_FromType(st, (PyObject *)Py_TYPE(value), &actual_info) < 0) { + return NULL; + } + if (expected_info != NULL && actual_info != NULL) { + PyErr_Format(PyExc_TypeError, + "expected %s with signature (restype=%R, argtypes=%R) " + "but received %s with signature (restype=%R, argtypes=%R)", + ((PyTypeObject *)type)->tp_name, + expected_info->restype ? expected_info->restype : Py_None, + expected_info->argtypes ? expected_info->argtypes : Py_None, + Py_TYPE(value)->tp_name, + actual_info->restype ? actual_info->restype : Py_None, + actual_info->argtypes ? actual_info->argtypes : Py_None); + return NULL; + } + } PyErr_Format(PyExc_TypeError, "expected %s instance instead of %s", ((PyTypeObject *)type)->tp_name,