2025, Oct 19 07:00

Avoid Segfaults When Embedding Python in C with Cython: Initialize and Import the Module Before Calling

Learn how to embed Python in C with Cython without segfaults: register with PyImport_AppendInittab, call Py_Initialize, import the module, ensure sys.path.

Bridging C and Python via Cython is a common pattern, but there is a subtle initialization step that can make or break your embedding story. A minimal “Hello World” that prints from Python and is called from a C program will segfault if the Cython module is not properly initialized and imported before use.

Problem setup

Consider a tiny Python function exposed through a Cython wrapper and invoked from C. The goal is to print “Hello World” from Python while the entry point is C.

# greetmod.py

def say_hi():
  print("Hello World")
# bridge.pyx

from greetmod import say_hi

cdef public void invoke_hi():
  say_hi()
/* main.c */

#include <Python.h>
#include "bridge.h"

int
main()
{
  Py_Initialize();
  invoke_hi();
  Py_Finalize();
}

Built this way, running the binary leads to a crash. A backtrace shows a failure while trying to resolve a module-global name, similar to a segfault within a call like __Pyx__GetModuleGlobalName with a null name pointer.

What’s really going on

Even though the wrapper function is declared public in Cython, the wrapper module itself is still a Python module. Its Python-level initialization must run before any of its code or globals are usable. That includes the line from greetmod import say_hi. If you jump straight into the exported C function without importing the Cython module, the module’s global namespace is uninitialized, so accessing its globals causes a crash.

The correct sequence is to register the module’s init function, initialize the Python interpreter, ensure Python can find your modules on sys.path, and then import the Cython module. Only after that is it safe to call the public function.

Fix and working code

The embedding flow needs three key steps: add the module init function with PyImport_AppendInittab before interpreter startup, initialize Python with Py_Initialize, and import the module with PyImport_ImportModule so its initialization and imports run. You also need to make sure Python can locate your modules, since the current working directory is not automatically on sys.path in an embedded scenario.

/* main.c (fixed) */

#include <Python.h>
#include <stdio.h>
#include "bridge.h"

int
main()
{
    PyObject *modref;

    /* Register "bridge" as a built-in module before Py_Initialize.
       PyInit_bridge is generated by Cython from bridge.pyx. */
    if (PyImport_AppendInittab("bridge", PyInit_bridge) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }

    /* Start the Python interpreter. */
    Py_Initialize();

    /* Optionally set the import path so Python can find your modules. */
    // PyRun_SimpleString("import sys\nsys.path.insert(0,'')");

    /* Import the Cython module so its initialization and imports run. */
    modref = PyImport_ImportModule("bridge");
    if (!modref) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'bridge'\n");
        goto fail;
    }

    /* Safe to call the public function now. */
    invoke_hi();

    Py_Finalize();
    return 0;

fail:
    Py_Finalize();
    return 1;
}

If you prefer an environment-based approach to adjusting the import path, set PYTHONPATH to the current directory when launching your program so the interpreter can locate greetmod.py and the generated module:

PYTHONPATH=. ./main

Alternatively, keep the inline approach shown above by inserting the empty string into sys.path with PyRun_SimpleString after Py_Initialize and before the first import.

Why this matters

Skipping the import step is not a harmless shortcut. The C code produced by Cython is designed to be imported as a Python module; the import drives the module’s initialization, including execution of its top-level Python code. If you skip initialization and run the exported functions directly, crashes are likely. The import can be deferred until you need the module, but it cannot be omitted.

It’s about the when of the import, not the if. The module must be imported; you can choose when to do it, but not whether to do it.

Takeaways

When embedding Python and calling into Cython-generated code from C, treat your wrapper as a normal Python module: register it with PyImport_AppendInittab before starting the interpreter, initialize Python, make sure sys.path includes your module location, import the module so its initialization runs, and only then call the public functions. This small bit of bootstrapping prevents hard-to-debug segfaults and makes a minimal “Hello World” reliable.

The article is based on a question from StackOverflow by vibe and an answer by Dunes.