Why must a ctypes foreign function have argtypes and restype set?
answer
- ctypes never sees the C header
- The declaration is the whole contract
- An undeclared result is a 32-bit int
- Only None, int, bytes, str pass undeclared
- Wrong register class, silent nonsense value
basics
~20 sWithout them ctypes guesses: it decodes the result as a C int, truncating any 64-bit pointer, and passes arguments by default rules that ignore the real signature. Declaring argtypes and restype makes ctypes convert and type-check every call.
solid answer
~50 s`ctypes` never reads the library's header, so the declaration you write is the entire contract. An undeclared `restype` defaults to `ctypes.c_int`: a function returning `int` works by luck, while a returned pointer is truncated to 32 bits and a returned `double` never passed through an integer register at all, so you read garbage. Without `argtypes`, only `None`, integers, `bytes` and `str` are accepted — a Python `float` raises `ctypes.ArgumentError` — and an accepted-but-wrong value, such as an `int` where the C prototype says `double`, is placed in the wrong register class and silently produces a nonsense result. Declaring `argtypes` gives conversion and up-front checking that names the bad argument's position; `restype = None` declares `void`, and the `errcheck` hook turns a failure return into a Python exception. Nothing verifies your declaration against the library — a wrong one is undefined behaviour, usually a segfault.
code
pycon · 12 lines>>> import ctypes
>>> f = ctypes.pythonapi.PyLong_FromLong
>>> f.restype
<class 'ctypes.c_int'>
>>> f.argtypes is None
True
>>> f(1.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
f(1.5)
~^^^^^
ctypes.ArgumentError: argument 1: TypeError: Don't know how to convert parameter 1go deeper
Remember the rule of thumb: declare argtypes and restype for every function you call, right after loading the library. Know that the default result type is a C int, not 'nothing'.
Be ready to explain the mechanics — which Python objects convert without argtypes, why a returned pointer is truncated by the default restype, and why a wrong declaration corrupts rather than raises.
Show how you keep declarations trustworthy over time: size assertions against the C side, smoke calls on every declared function, errcheck for error translation, and a single place where prototypes live.
Frame the risk: hand-transcribed declarations are an unverified contract that fails as a process crash, not an exception. Decide whether that belongs in a long-lived service at all, or behind a subprocess boundary.
### The declaration is the whole contract `ctypes` never reads the library's header file. Nothing tells it that `render_digest` takes a `const char *` and a `size_t` and returns a `double`. The `argtypes` and `restype` attributes you set on the foreign function object **are** that information, and everything `ctypes` does at call time — which registers the arguments go in, how many bytes to read back, whether a Python object needs converting first — is derived from them. Get them wrong and you have lied to the machine: the C function reads whatever happens to be in the registers, and the process typically dies with a segmentation fault instead of raising something you can catch. ### What happens when restype is not set `restype` defaults to `ctypes.c_int`. On a 64-bit platform that is a **signed 32-bit** decode. For a function that really returns `int` the default is harmless, which is exactly why the trap survives so long: the first thing you try works. The moment the function returns a pointer, a `long`, a `size_t` or a `double`, the default is wrong. A returned pointer is truncated to its low 32 bits and sign-extended, so the address you then use — by passing it back into the library, or by handing it to `ctypes.string_at` — is garbage, and dereferencing it is a crash if you are lucky and silent corruption if you are not. A returned `double` never travelled in an integer register at all, so the number you read is unrelated to the result. Declare `restype = None` for a `void` function; nothing else says "there is no result". `restype` also accepts a Python callable instead of a type, in which case `ctypes` passes the integer result to it and returns whatever it gives back. The more precise hook is `errcheck`, which receives the result, the function object and the argument tuple, and is the natural place to turn a failure return into a Python exception carrying `ctypes.get_errno()`. ```python lib = ctypes.CDLL(path, use_errno=True) lib.render_digest.argtypes = [ctypes.c_char_p, ctypes.c_size_t] lib.render_digest.restype = ctypes.c_double ``` ### What happens when argtypes is not set Without `argtypes`, `ctypes` falls back to default conversions and accepts only a narrow set of Python objects: `None` (passed as a NULL pointer), integers, `bytes` and `str`. A `bytes` object goes as `char *`, a `str` as `wchar_t *`, and an `int` as a C `int`. Everything else — a Python `float` included — is rejected up front with `ctypes.ArgumentError: argument 1: TypeError: Don't know how to convert parameter 1`. That rejection is the friendly case. The dangerous case is accepted-but-wrong. A Python `int` handed to a function whose parameter is a `double` is placed in an integer register while the C function reads a floating-point one, so you get a plausible-looking wrong number and no error at all. A Python `int` passed where the prototype says `long` truncates on the way in the same way a wrong `restype` truncates on the way out. And a `str` passed where the C side expects `char *` hands over a `wchar_t *`: the bytes the library reads are the wrong width entirely. Declaring `argtypes` buys three things. **Checking**: a value that cannot be converted raises `ctypes.ArgumentError` before the call, naming the argument's position. **Conversion**: with `argtypes = [ctypes.c_char_p]` a `bytes` object is converted for you, and with `argtypes = [ctypes.POINTER(ctypes.c_int)]` a `ctypes.byref(...)` reference is accepted where a raw object would not be. **Extension**: any object exposing a `from_param` classmethod is accepted too, which is how a domain object can be passed straight through with its own conversion logic. ### Variadics and platform ABI An `argtypes` list may be shorter than the actual call for a variadic function such as a `printf`-style logger: the declared entries cover the fixed parameters and the rest are passed by the default rules. On some ABIs — notably Apple silicon — fixed and variadic arguments are passed through different mechanisms, and `ctypes` can only get that right if you have told it where the fixed part ends. Setting `argtypes` on a variadic function is therefore mandatory there rather than merely advisable. ### Prototypes as objects `ctypes.CFUNCTYPE(restype, *argtypes)` expresses the same contract as a reusable prototype **type** rather than as attributes on one function object. That is what you need for a callback you hand to C, and for a function pointer stored inside a `ctypes.Structure` field, since in both cases there is no loader attribute to hang `argtypes` on. Calling a `CFUNCTYPE` instance from Python performs exactly the same conversion and checking, which makes it a convenient way to see the rules in action without a real library. ### Keeping the declarations honest Nothing in the system verifies any of this against the library — not at import, not at call time, not ever. Two habits close the gap. First, assert `ctypes.sizeof` for every type you declare against the size the C side reports, since a size mismatch means the ABI you assumed is not the ABI you have. Second, keep the declarations in one small module beside a transcription of the C prototypes they came from, so a library upgrade has a single obvious place to be re-checked, and exercise each declared function once in a smoke test: a wrong declaration nearly always fails on the first real call, which is a far better place to find it than hour four of an overnight run.
- How do you declare a C function that returns void?Set `restype = None`. Leaving it unset does not mean 'no result' — it means `ctypes.c_int`, so ctypes reads a return register that the function never wrote and hands you a meaningless integer. `None` is the only spelling that says there is nothing to decode.
- What does the errcheck attribute give you that restype does not?`restype` says how to decode the raw result. `errcheck` is a callable invoked afterwards with the decoded result, the function object and the argument tuple, so it can inspect the arguments too — the natural place to raise `OSError` when the return value signals failure, using `ctypes.get_errno()` on a library loaded with `use_errno=True`. It can also return a different object, replacing the result entirely.
- If nothing checks your declaration against the library, how do you keep it honest?Treat the prototypes as transcribed source: keep them in one small module beside a copy of the C declarations they came from, so a library upgrade has an obvious place to be re-checked. Assert `ctypes.sizeof` for every struct and integer type you declare against the sizes the C side reports, and exercise each declared function once in a smoke test — a wrong declaration usually crashes on the first real call rather than corrupting quietly.
Declaring argtypes and restype is filling in the customs form for every value crossing the border; leave it blank and the border guard stamps everything as a 32-bit integer and waves it through.
saying these in an interview costs you the question
- Thinks an unset restype means the function returns nothing
- Believes ctypes infers the signature from the library
- Says a wrong declaration raises a catchable Python exception
- Passes a Python float without argtypes and expects a C double
- Assumes returning a pointer works fine on 64-bit by default
- Treats argtypes as optional documentation