Skip to content

C extensions & alternative implementations

Senior

ctypes/cffi/Cython/pybind11 trade-offs, releasing the GIL around C calls, and CPython vs PyPy vs Jython/IronPython.

Created 2 September 2026 · Updated 12 September 2026

13 concepts · detail hidden

  1. 01Approaches for calling C/C++ from Python — `ctypes`, `cffi`, raw `Python.h`, Cython, pybind11

    ctypes: part of the standard library, requiring no separate compilation step at all, but genuinely verbose and error-prone since every argument/return type has to be manually, explicitly declared, and it’s limited to plain C-compatible interfaces without significant extra wrapping work for anything C+±specific. cffi: offers a somewhat cleaner API than ctypes, with better performance in certain usage modes, but remains fundamentally C-oriented. A raw CPython C extension (writing directly against Python.h): offers maximum control and raw performance, but is genuinely verbose, requires careful manual reference-count management (a real, ongoing risk of refcount bugs, memory leaks, or outright crashes if handled incorrectly), and has a real, non-trivial learning curve. Cython: lets you write Python-like syntax that compiles down to C, excellent for incrementally optimizing specific hot sections of existing Python code, or wrapping existing C libraries with dramatically less boilerplate than a raw extension would require. pybind11: header-only, purpose-built specifically for exposing C++ classes/templates cleanly to Python — the modern default, standard choice specifically when the underlying native code you’re binding is genuinely C++ rather than plain C.

    #c-approaches-for-calling-cc-from-python-ctypes-cffi-raw-python
  2. 02`ctypes` `argtypes` and `restype` — the memory-safety consequences of getting them wrong

    By default, ctypes simply assumes every function argument is a plain C int and every return value is also int, unless you explicitly tell it otherwise — calling a real function with these wrong default assumptions can silently misinterpret the actual raw bytes involved (for instance, truncating or fundamentally misreading a genuine double return value as though it were merely an int), and in more serious cases involving pointer arguments, can cause genuine memory corruption or outright segfaults, since ctypes has fundamentally no way to independently verify correctness against the real, actual C function signature — it simply, blindly trusts whatever you explicitly declared to it. Explicitly setting argtypes and restype accurately is the closest thing to a genuine safety net ctypes offers here, and skipping this step is a real, genuinely dangerous habit in any production code that touches native C libraries.

    #c-ctypes-argtypes-and-restype-the-memory-safety-consequences-o
  3. 03Releasing the GIL around a long-running C call — `Py_BEGIN_ALLOW_THREADS` and its hazards

    A C extension function holds the GIL by default, exactly like ordinary Python-level code does — meaning a genuinely long-running, pure-C computation with no Python API calls inside it still fully blocks every other Python thread the entire time, even though the C code itself has no actual technical need for the GIL’s protection during that stretch. The extension author has to explicitly wrap that long-running C section with the Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS macros to deliberately release and later reacquire the GIL around it — critically, no Python C-API calls are safe to make anywhere inside that released window, which is a genuine, real source of subtle crashes if an extension author releases the GIL and then accidentally, even briefly, touches a Python object somewhere inside that released section.

    #c-releasing-the-gil-around-a-long-running-c-call-py_begin_allo
  4. 04What actually happens when you call `numpy_array.sum()` — Python API over a C core

    Almost the entirety of numpy’s actual numeric core is implemented in C (and, increasingly, SIMD-vectorized C leveraging compiler intrinsics for extra hardware-level parallelism) — calling .sum() genuinely isn’t looping in Python at all under the hood; it’s a single call straight down into compiled C code operating directly on contiguous memory — which is precisely why numpy operations run dramatically faster than an equivalent hand-written pure-Python loop, despite the code superficially “being Python.” Most of the everyday performance benefit people casually attribute to simply “using numpy/pandas” is really “using C/Fortran through a convenient Python-shaped API.”

    #c-what-actually-happens-when-you-call-numpy_arraysum-python-ap
  5. 05What `cdef int x` in Cython buys you — skipping Python's object machinery

    An ordinary, untyped Python variable requires genuine dynamic dispatch and real boxing/unboxing overhead on essentially every single operation performed on it (the broader underlying “everything is fundamentally a PyObject*” cost that pervades ordinary Python execution). Declaring cdef int x in Cython lets the Cython compiler generate actual C code that operates directly on a genuine, raw C int, entirely skipping Python’s usual object machinery for that specific variable altogether. The resulting performance win scales directly with how much of a given hot loop’s variables actually get statically typed this way — which is precisely why “sprinkle cdef selectively on a hot loop’s key variables” is such a common, genuinely high-leverage optimization pattern in practice, distinct from and often preferable to rewriting the entire routine in raw C by hand.

    #c-what-cdef-int-x-in-cython-buys-you-skipping-pythons-object-m
  6. 06What `pybind11` solves that `ctypes` and raw `Python.h` can't — classes, templates, exceptions, RAII

    C++ genuinely has classes, templates, operator overloading, exceptions, and RAII (resource acquisition is initialization, a core C++ idiom tying resource lifetime directly to object lifetime) — none of which map cleanly at all onto the flat, simple function-pointer model that ctypes and the raw CPython C API are fundamentally built around, since those are designed around plain C’s much simpler feature set. pybind11 (itself header-only, implemented using heavily templated modern C++) provides genuinely ergonomic, type-safe machinery specifically for binding real C++ classes cleanly as Python classes, correctly translating C++ exceptions into equivalent Python exceptions, and correctly managing C++ object lifetimes/RAII semantics against Python’s own separate reference-counting system — solving a genuinely, fundamentally harder interoperability problem than plain C bindings ever need to address.

    #c-what-pybind11-solves-that-ctypes-and-raw-pythonh-cant-classe
  7. 07Caller responsibility for C-allocated memory returned through `ctypes`

    You’re fully responsible for determining, from the C library’s own documented conventions, whether it expects you (the caller) to explicitly free that memory yourself later (via a corresponding C free() call, or the library’s own specific dedicated free function) or whether the library itself retains ongoing ownership and handles freeing it internally. Python’s garbage collector has absolutely no visibility whatsoever into this raw, C-allocated memory — it’s completely invisible to Python’s own memory management entirely — so getting this responsibility wrong produces either a genuine memory leak (memory that’s never freed by anyone) or an outright crash (a double-free, or attempting to free memory that Python itself doesn’t actually own or understand at all). This is exactly the specific class of bug that Python’s own internal memory-safety guarantees simply don’t, and structurally can’t, protect you from, the moment you cross the FFI (foreign function interface) boundary into native code.

    #c-caller-responsibility-for-c-allocated-memory-returned-throug
  8. 08When not to write a C extension despite the speed — the ongoing costs

    Real production judgment, not merely enthusiasm for a raw performance win: genuine build complexity (compiling correctly per-platform, real wheel-distribution/packaging headaches for anyone installing your package); a meaningfully smaller pool of engineers who can safely, confidently maintain C/C++ code compared to plain Python going forward; genuinely harder debugging (segfaults and native crash dumps instead of clean, readable Python tracebacks); and a real, structural risk of introducing entirely new classes of memory-safety bugs that plain Python simply cannot have at all. Worth the real cost specifically when the performance win is both large and genuinely durable over time — and ideally only after actual profiling has concretely, empirically proven the bottleneck genuinely lives there, rather than assuming it does.

    #c-when-not-to-write-a-c-extension-despite-the-speed-the-ongoin
  9. 09CPython vs PyPy — what JIT compilation means and which workloads it speeds up

    CPython compiles Python source down to an intermediate bytecode representation and then straightforwardly interprets that bytecode directly, every single time it runs, with no further optimization applied based on actual runtime behavior. PyPy’s JIT (just-in-time compiler) instead actively observes running code as it actually executes, identifies genuinely “hot” loops that are being run repeatedly, and compiles specifically those identified paths down into real, optimized machine code at runtime — entirely skipping the ordinary interpretation overhead specifically for the code paths that actually matter most to overall performance. This produces genuinely large speedups specifically for long-running, loop-heavy pure-Python workloads, since that pattern is precisely what JIT compilation, as a general technique, is designed to exploit effectively.

    #c-cpython-vs-pypy-what-jit-compilation-means-and-which-workloa
  10. 10Why PyPy isn't the default despite being faster — C-extension compatibility, warm-up, memory

    C-extension compatibility is the biggest, most concrete factor: many genuinely popular Python packages (historically including numpy, pandas, and plenty of other C-extension-heavy libraries) have either not worked at all under PyPy, worked only with meaningfully reduced performance (largely defeating the whole point of switching), or have simply lagged noticeably behind in ongoing support. JIT warm-up time means genuinely short-lived scripts or CLI tools can actually run slower under PyPy than plain CPython, since the JIT simply hasn’t had enough time to identify and compile any hot paths before the process has already exited. And PyPy can, for certain workloads, genuinely use more memory than CPython despite running faster overall — a real, concrete tradeoff that surprises people who naively expect “faster” to straightforwardly mean “strictly better across every dimension.”

    #c-why-pypy-isnt-the-default-despite-being-faster-c-extension-c
  11. 11Which workloads suit PyPy and which make migrating a waste of effort

    Genuinely good fit: long-running services performing heavy, genuinely pure-Python computation inside tight, repeatedly-executed loops, with minimal reliance on unsupported or poorly-supported C extensions. Genuinely poor fit: I/O-bound services (where the actual bottleneck isn’t CPU/interpretation speed at all, so JIT compilation buys essentially nothing); short-lived scripts (where JIT warm-up cost simply dominates any potential benefit); or anything already heavily built on numpy/pandas/scikit-learn-style C-extension-heavy numerical stacks — since that particular code is already running at genuine native C speed even under plain CPython, meaning PyPy’s JIT has no meaningful ability to further speed up code it fundamentally doesn’t control anyway, while still potentially introducing real compatibility risk for essentially no corresponding gain.

    #c-which-workloads-suit-pypy-and-which-make-migrating-a-waste-o
  12. 12Whether PyPy has a GIL — and why switching doesn't unlock threaded CPU parallelism

    PyPy’s default, standard implementation still genuinely has a GIL, subject to the exact same fundamental “only one thread executes bytecode at any given instant” constraint as CPython — switching to PyPy does not solve the “threading doesn’t give genuine CPU parallelism” problem at all; it only meaningfully helps overall single-threaded execution speed. (There was an experimental PyPy-STM project exploring software transactional memory specifically as a path toward removing the GIL entirely, but it never reached genuine mainstream production readiness.)

    #c-whether-pypy-has-a-gil-and-why-switching-doesnt-unlock-threa
  13. 13Other alternative Python implementations — Jython, IronPython, GraalPython, MicroPython

    Jython: compiles Python code down to genuine JVM bytecode, letting Python code directly call Java libraries and run natively on the JVM — valuable specifically for organizations already deeply embedded in a Java-centric ecosystem who want Python’s ergonomics alongside genuinely native Java interoperability. IronPython: the direct .NET-ecosystem equivalent of the same basic idea. GraalPython (built on GraalVM): a newer, polyglot-VM-based implementation aiming for both competitive speed and genuine interoperability with other languages hosted on the same GraalVM platform. MicroPython: a genuinely from-scratch reimplementation specifically targeting microcontrollers and other severely memory-constrained embedded systems — not really a “CPython compatibility” play in the same sense as the others at all.

    #c-other-alternative-python-implementations-jython-ironpython-g