2025, Nov 10 15:00
How to Pass Strings to a Windows DLL with ctypes: Avoid ArgumentError, use c_char_p bytes or c_wchar_p Unicode
Learn why ctypes raises ArgumentError when passing str to a Windows DLL and how to fix it: bytes vs str, c_char_p vs c_wchar_p, ANSI vs Unicode. Avoid bugs.
Bridging Python and a Windows DLL looks straightforward until text encoding sneaks in. A small mismatch between bytes and str is enough to trigger a ctypes.ArgumentError and block the call. Here is how to recognize the pattern, why it happens, and how to fix it cleanly without changing program logic.
Reproducing the issue
Consider a minimal exported function from a Windows DLL that forwards a file path to another routine and returns a bool:
extern "C" {
bool __declspec(dllexport) ApiOpenPath(const char* pPath)
{
return InnerInvoke(pPath);
}
}
And a Python 3.12.9 caller using ctypes:
import ctypes
mod = ctypes.cdll.LoadLibrary('The.dll')
open_entry = mod.ApiOpenPath
open_entry.argtypes = [ctypes.c_char_p]
open_entry.restype = ctypes.c_bool
path_name = "TheOutput.txt"
res = open_entry(path_name)
print(res)
Invoking this code produces an error similar to:
ctypes.ArgumentError: argument 1: TypeError: wrong type
What actually goes wrong
ctypes.c_char_p expects a bytes object. The "TheOutput.txt" literal is a Python str. On Windows, C APIs that take char* for paths interpret those bytes as ANSI-encoded. With pure ASCII, a bytes literal works. With characters like ü in a filename, a bytes literal can’t represent it directly and even writing b'pingüino.jpg' is invalid syntax. In contrast, Python str aligns with the C wide-character type wchar_t* and ctypes.c_wchar_p.
Two ways to fix it
If you keep the DLL signature as const char*, pass bytes from Python. For ASCII-only names, use a bytes literal. For names that include non-ASCII characters, pass a str encoded to bytes via ANSI.
import ctypes
libh = ctypes.cdll.LoadLibrary('The.dll')
call_open = libh.ApiOpenPath
call_open.argtypes = [ctypes.c_char_p]
call_open.restype = ctypes.c_bool
# ASCII is fine as bytes
ascii_ok = b"TheOutput.txt"
print(call_open(ascii_ok))
# For non-ASCII, encode to ANSI
non_ascii = 'pingüino.jpg'.encode('ansi')
print(call_open(non_ascii))
The ANSI rule is easy to see with a standard library call. Example using C FILE *fopen(const char *filename, const char *mode) and Python 3.13:
>>> import ctypes as cx
>>> crt = cx.CDLL('msvcrt')
>>> crt.fopen.argtypes = cx.c_char_p, cx.c_char_p
>>> crt.fopen.restype = cx.c_void_p
>>> crt.fopen(b'pingüino.jpg', b'r')
File "<python-input>", line 1
crt.fopen(b'pingüino.jpg', b'r')
^^^^^^^^^^^^^^^
SyntaxError: bytes can only contain ASCII literal characters
>>> crt.fopen('pingüino.jpg'.encode('ansi'), b'r')
140704303274784
If you can change the DLL, switch the interface to wide strings. Using wchar_t* on the C side and ctypes.c_wchar_p on the Python side lets you pass any supported Unicode filename directly as a Python str.
extern "C" {
bool __declspec(dllexport) ApiOpenPathW(const wchar_t* pWidePath)
{
return InnerInvokeW(pWidePath);
}
}
import ctypes as cx
dllh = cx.CDLL('The.dll')
wide_open = dllh.ApiOpenPathW
wide_open.argtypes = [cx.c_wchar_p]
wide_open.restype = cx.c_bool
print(wide_open('pingüino.jpg'))
The same principle applies to standard C wide I/O. Example using C FILE *_wfopen(const wchar_t *filename, const wchar_t *mode):
>>> import ctypes as cx
>>> msv = cx.CDLL('msvcrt')
>>> msv._wfopen.argtypes = cx.c_wchar_p, cx.c_wchar_p
>>> msv._wfopen.restype = cx.c_void_p
>>> msv._wfopen('pingüino.jpg', 'r')
140704303274928
Why this matters
The boundary between Python and a native Windows DLL is strict about types. Passing str where the C side expects char* leads to immediate type errors, and encoding mismatches on file paths can cause subtle bugs that only show up with real-world filenames. Understanding how ctypes maps Python types to C pointers avoids wasted time and fragile workarounds.
Conclusion
Match the pointer type to the correct Python type. If the DLL expects const char*, pass bytes and, when needed, encode the filename with .encode('ansi'). If you control the native API, prefer wchar_t* and use ctypes.c_wchar_p so that Python str works seamlessly with Unicode paths. Keeping this contract clear at the FFI boundary makes your Windows DLL interop predictable and robust.