skip to content

ctypes and Foreign Functions

Calling into a shared library with ctypes: loading it, declaring argument and result types, and passing structs and pointers. A wrong declaration segfaults the process instead of raising.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

Why must a ctypes foreign function have argtypes and restype set?

level: middleimportance: must knowfreq 38%

answer

  1. ctypes never sees the C header
  2. The declaration is the whole contract
  3. An undeclared result is a 32-bit int
  4. Only None, int, bytes, str pass undeclared
  5. Wrong register class, silent nonsense value

basics

~20 s

Without 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
pycon
>>> 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 1

go deeper

for a junior

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'.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

What does ctypes.CDLL do, and how do you call a function from that library?

level: juniorimportance: should knowfreq 30%

basics

~20 s

ctypes.CDLL loads a shared library into the running process through the platform's dynamic loader. Attribute access on the object it returns resolves an exported C symbol by name and hands back a callable you invoke like a normal function.

open as a page

How do you define a ctypes.Structure and pass it to C by reference?

level: middleimportance: should knowfreq 26%

basics

~10 s

Subclass ctypes.Structure and set fields to an ordered list of (name, type) pairs, which fixes the memory layout. Declare the parameter as ctypes.POINTER(YourStruct) in argtypes and pass ctypes.byref(instance) at the call.

open as a page

A ctypes callback segfaults during a 6-hour nightly email-digest run, and the restart re-sends digests. How do you diagnose it?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Suspect object lifetime: C keeps a raw address and no Python reference, so a collected callback object or buffer leaves it dereferencing freed memory. Enable faulthandler for a traceback and re-run under -X dev to make it deterministic.

open as a page