Skip to content

Ctypes: Unclear Error Message for WinFunctionType Mismatch in Windows API Calls #125788

Open

Description

@songyuc

Description:

When working with Windows API functions (such as SetWindowsHookExW) using ctypes, providing a HOOKPROC callback function of an incorrect type results in an unclear error message. This can be confusing, especially for beginners or when debugging complex scenarios.

Current error message:
TypeError: expected WinFunctionType instance instead of WinFunctionType

Suggested improved error message:
TypeError: expected WinFunctionType with signature (restype=wintypes.LPARAM, argtypes=[ctypes.c_int32, wintypes.WPARAM, wintypes.LPARAM]) but received WinFunctionType with signature (restype=ctypes.c_int, argtypes=[ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM])

Steps to Reproduce:

  1. Create a Python script with the following content:
import ctypes
import ctypes.wintypes

HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM)

user32 = ctypes.windll.user32
user32.SetWindowsHookExW.argtypes = [ctypes.c_int, HOOKPROC, ctypes.wintypes.HINSTANCE, ctypes.wintypes.DWORD]
user32.SetWindowsHookExW.restype = ctypes.wintypes.HHOOK

def callback(nCode, wParam, lParam):
    return user32.CallNextHookEx(None, nCode, wParam, lParam)

pointer = HOOKPROC(callback)
hook_id = user32.SetWindowsHookExW(13, pointer, None, 0)  # 13 corresponds to WH_KEYBOARD_LL

# At this point, import pynput, which modifies the SetWindowsHookExW function
from pynput import keyboard, mouse

# Attempt to set the hook again, which now fails due to type mismatch
hook_id = user32.SetWindowsHookExW(13, pointer, None, 0)
  1. Run the script

Expected Result:
A clear error message indicating the mismatch between the expected and provided WinFunctionType, including details about their signatures.

Actual Result:
An unclear error message: "TypeError: expected WinFunctionType instance instead of WinFunctionType"

Python Version: 3.12.7
Operating System: Windows

Suggested Improvement:
Modify the logic in ctypes that generates these error messages to include more detailed information about the expected and actual WinFunctionType instances, such as their return types and argument types. This would greatly improve the readability and usefulness of the error message, especially for beginners and when debugging complex scenarios.

Additional Notes:
This issue becomes apparent when using libraries like pynput that modify Windows API function signatures. A more descriptive error message would help users identify and resolve such conflicts more easily.

CPython versions tested on:

3.12

Operating systems tested on:

Windows

Activity

  1. added
    type-featureA feature request or enhancement
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Oct 21, 2024
  2. zooba commented on Oct 21, 2024

    @zooba
    Member

    Improving an error message is a feature, not a bug, but there shouldn't be any reason why it can't be done here (it just won't be backported to maintenance releases).

    (The bugfix in pynput (or the user's own code) is to use ctypes.WinDLL("user32") rather than ctypes.windll.user32 so that they aren't modifying shared objects. This should already be documented, I'm pretty sure.)

  3. songyuc commented on Oct 29, 2024

    @songyuc
    Author

    Improving an error message is a feature, not a bug, but there shouldn't be any reason why it can't be done here (it just won't be backported to maintenance releases).

    (The bugfix in pynput (or the user's own code) is to use ctypes.WinDLL("user32") rather than ctypes.windll.user32 so that they aren't modifying shared objects. This should already be documented, I'm pretty sure.)

    Thanks sincerely for your guide!

  4. diegogimenez04 commented on Jun 9, 2026

    @diegogimenez04

    Hello everyone. Is this still an issue? If not, I suggest closing it

  5. ZeroIntensity commented on Oct 20, 2026

    @ZeroIntensity
    Member

    Yes, this is still valid. This also isn't specific to Windows; here's a Linux repro:

    import ctypes
    
    libc = ctypes.CDLL(None)
    
    CMPFUNC_INT = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
    CMPFUNC_LONG = ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_void_p)
    
    libc.qsort.restype = None
    libc.qsort.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t, CMPFUNC_INT]
    
    def cmp(a, b):
        return 0
    
    libc.qsort(None, 0, ctypes.sizeof(ctypes.c_int), CMPFUNC_LONG(cmp))
    Traceback (most recent call last):
      File "/home/peter/develop/cpython/a.py", line 14, in <module>
        libc.qsort(None, 0, ctypes.sizeof(ctypes.c_int), CMPFUNC_LONG(cmp))
        ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ctypes.ArgumentError: argument 4: TypeError: expected CFunctionType instance instead of CFunctionType
    
  6. assilrguez commented on Oct 23, 2026

    @assilrguez

    I worked through a local proposal for this. The mismatch is handled by CDataType_from_param_impl() in Modules/_ctypes/_ctypes.c; callback function types currently reach its generic type error, where both types have the same tp_name.

    For mismatched ctypes callback types, the proposed special case uses each callback type's StgInfo to show the expected and actual return and argument types, while leaving the existing generic error unchanged for other mismatches:

    if (PyCFuncPtrTypeObject_Check(st, type) &&
        PyCFuncPtrObject_Check(st, value)) {
        StgInfo *expected_info, *actual_info;
        if (PyStgInfo_FromType(st, type, &expected_info) < 0 ||
            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), "
                         "got %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;
        }
    }

    The resulting message is in the familiar “expected …, got …” form, for example:

    expected CFunctionType with signature (restype=<class 'ctypes.c_float'>, argtypes=(<class 'ctypes.c_void_p'>,)), got CFunctionType with signature (restype=<class 'ctypes.c_double'>, argtypes=(<class 'ctypes.c_short'>,))
    

    I added a regression test in Lib/test/test_ctypes/test_parameters.py that passes a callback with a different restype and argtype and checks that both signatures appear in the TypeError. I also added a Misc/NEWS.d/next/Library entry.

    On my local Windows Release build, the focused test passed and the complete test_ctypes suite passed (701 tests run, 43 skipped). This proposal is for the in-development branch; I have not tested the reporter's exact pynput scenario.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions