skip to content

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

level: juniorimportance: should knowfreq 30%

answer

  1. No compiler, no header, no build step
  2. It is Python's dlopen
  3. Attribute lookup resolves an exported symbol
  4. find_library gives the platform filename
  5. CDLL releases the GIL, PyDLL holds it

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.

solid answer

~50 s

`ctypes.CDLL(name)` is Python's `dlopen`: it maps a shared library — a `.so`, `.dylib` or `.dll` — into the current process and returns a loader object. Looking up an attribute on it, `lib.render_digest`, resolves that exported C symbol and caches a callable foreign-function object; a symbol the library does not export raises `AttributeError`, and a library that will not load raises `OSError`. `ctypes.util.find_library("c")` turns a bare name into the platform's actual filename so you do not hardcode one. Nothing is compiled and no header is parsed, so before calling anything non-trivial you must declare the signature yourself with `argtypes` and `restype`. Functions called through `CDLL` use the C calling convention and release the GIL for the duration of the call; `ctypes.PyDLL` is the variant that keeps the GIL held, for functions that touch the CPython C API.

code

python · 9 lines
python
import ctypes
import ctypes.util

name = ctypes.util.find_library("c")   # platform-specific filename
libc = ctypes.CDLL(name)               # the library is now in this process

libc.abs.argtypes = [ctypes.c_int]     # never rely on the guesses
libc.abs.restype = ctypes.c_int
print(libc.abs(-42))

go deeper

for a junior

Be ready to say in one sentence that ctypes loads an existing compiled library and calls its functions, with no compiler involved, and to show the two steps: CDLL to load, attribute access to get the function.

for a middle

Explain what the loader object actually does on attribute access, why find_library exists, and why declaring argtypes and restype is not optional once the signature is anything but int-in, int-out.

for a senior

An interviewer expects you to weigh ctypes against a compiled extension for a real dependency: per-call marshalling cost, hand-written declarations nothing verifies, and crashes that kill the process instead of raising.

for a principal

Own the boundary policy: which libraries are allowed in-process at all, how their absolute paths are resolved rather than searched, who re-verifies the declarations on a library upgrade, and what a native crash does to your availability budget.

### What `ctypes` actually is `ctypes` is a standard-library foreign function interface. It lets pure Python call functions that live in an already-compiled shared library — `.so` on Linux, `.dylib` on macOS, `.dll` on Windows — without writing, compiling or shipping any C of your own. There is no build step, no header parsing and no generated wrapper module: at runtime, `ctypes` asks the operating system's dynamic loader for the library, looks up symbols in it, marshals your Python arguments into C values, makes the call, and marshals the result back. ### `ctypes.CDLL` is the load `ctypes.CDLL(name)` is the `dlopen` of Python. It maps the named library into the current process and returns a loader object. If the loader cannot find or link the file you get an `OSError` carrying the platform's own message (a missing dependency of the library shows up here too, which is why the message is often about a library you never named). The load is process-wide and permanent for the life of the process: loading the same file twice hands you the same underlying image. Attribute access on that object is the symbol lookup. `lib.render_digest` asks the loader for the exported symbol `render_digest`, wraps its address in a foreign-function object, caches it on the loader instance, and returns it. A symbol the library does not export raises `AttributeError` — not `ImportError`, which trips people up. For names that are not valid Python identifiers, or that collide with `ctypes` internals, `getattr(lib, "some.odd$name")` and `lib["render_digest"]` both work. ### Naming the library portably Library filenames are platform-specific — `libc.so.6`, `/usr/lib/libc.dylib`, `msvcrt.dll` — so hardcoding one makes your code single-platform. `ctypes.util.find_library("c")` performs the platform's own search and returns the filename to hand to `CDLL`, or `None` when nothing matches. For a library you ship yourself, resolve an absolute path relative to your own package directory rather than trusting the loader's search order; that also avoids loading an attacker-controlled file that happens to sit earlier on the search path. ### Calling the function The returned foreign function is callable with ordinary Python arguments: ```python libc.abs(-42) ``` That works, but only because `abs` takes and returns a C `int` and `ctypes` guesses `int` in both directions. For anything else you must declare the signature yourself by setting the function's `argtypes` (a sequence of `ctypes` types) and `restype` (a single type, or `None` for `void`). `ctypes` never sees the library's header, so a declaration that does not match the real C prototype is undefined behaviour, and the usual symptom is a segmentation fault that kills the process rather than an exception you can catch. ### `CDLL` versus `PyDLL` `ctypes.CDLL` uses the platform's standard C calling convention and **releases the GIL around every call**, so a long native call made through it does not block other Python threads. That is the right default for third-party libraries, and it is also why the function must not touch any Python object while it runs. `ctypes.PyDLL` is the same thing with the GIL held for the duration; you need it for functions that call into the CPython C API or set the Python error indicator. `ctypes.pythonapi` is a ready-made `PyDLL` pointing at the interpreter itself. On Windows there is additionally a `stdcall` loader for the Win32 convention, which is a separate class from `CDLL`. Two convenience loader objects exist as well: `ctypes.cdll` is a `LibraryLoader` on which attribute access loads a library rather than a symbol, so `ctypes.cdll.LoadLibrary(name)` is an alternative spelling of the constructor. ### Errors from the C side C functions report failure through their return value plus a global `errno`, and `ctypes` will not raise for you. Pass `use_errno=True` to `CDLL` and `ctypes` keeps a private copy of `errno` per thread, readable with `ctypes.get_errno()` and settable with `ctypes.set_errno()` — a private copy because any intervening Python code could otherwise clobber the real one before you read it. ### When to reach for it `ctypes` is the cheapest way to call a handful of functions in a stable C library: no compiler on the build machine, no wheel per platform, nothing to keep in sync with the interpreter's ABI. It pays for that with per-call marshalling overhead and with hand-written declarations that nothing verifies, so it is a poor fit for a chatty interface called in a tight loop, or for a C++ API, which does not export plain C symbols at all unless it was written to.

  • When would you use ctypes.PyDLL instead of ctypes.CDLL?
    `CDLL` releases the GIL for the duration of every call, so the called code must not touch any Python object. `PyDLL` keeps the GIL held, which is required for functions that call into the CPython C API or set the Python error indicator — `ctypes.pythonapi` is exactly such a loader. For an ordinary third-party library, `CDLL` is right: holding the GIL through a long native call would block every other Python thread.
  • What error do you get for a library that will not load versus a symbol that does not exist?
    A library the loader cannot find or link raises `OSError` from the `CDLL` call, carrying the platform's message — which often names a missing dependency rather than the file you asked for. A symbol the library does not export raises `AttributeError` at attribute-lookup time, not `ImportError`. Guarding with `hasattr` is the usual way to feature-detect an optional function.
  • Why does ctypes not need the library's header file, and what does that cost you?
    There is no compile step, so nothing ever parses the C declarations — which is precisely why no compiler, no wheel per platform and no interpreter-ABI coupling are needed. The cost is that you transcribe every prototype by hand and nothing verifies it. A mismatch is undefined behaviour, and the usual symptom is a segmentation fault that kills the process rather than an exception you can catch.

It is a phone number, not a contract: the loader connects you to the function, but nobody tells you what language to speak — you supply the argument and result types yourself.

saying these in an interview costs you the question

  • Thinks ctypes compiles C or needs a compiler installed
  • Believes ctypes reads the library's header to learn signatures
  • Assumes one library filename is portable across platforms
  • Says a missing symbol raises ImportError
  • Thinks every ctypes loader releases the GIL, PyDLL included

context