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.
The article is based on a question from StackOverflow by user1752563 and an answer by Mark Tolonen.