EnglishEspañol
Creating a Python binding using Cython

Creating a Python binding using Cython


by Glendon Mooney, Pixel-Nexus | 3 February 2025

I recently found myself wanting the advantages of working with C/C++ along with the convenience that Python® offers. This led me to Cython.

Cython allows you to create a Python interface for your C/C++ code. This is especially useful for tasks that would otherwise take a long time or would be tedious to do in Python.

For my needs, I wanted to be able to parse binary files, process/interpret the contents, and add an interface to display the data using Python.

This article will guide you through creating a simple Python interface for C code using Cython.

Why Cython?

Cython allows you to write raw C code without the need to add any CPython-related headers or definitions that would otherwise clutter up your C code. Cython also uses Python's syntax making it more familiar/comfortable for Python developers.

Example Cython binding

In this example, we will take a look at how we can create a Cython binding so that we can call our C++ code within Python!

First remarks

You can clone the example_cython_binding to use as a starting point.


git clone https://github.com/Pixel-Nexus/example_cython_binding

You should see a setup.py file along with a few folders, cython and src. The src folder is where the C++ code will be stored. The cython folder is where the binding will be written.

In the cython folder we have two files. One is named extension.pxd and the other is named interface.pyx. Cython will compile our interface.pyx file to C code. The C code is then compiled to a .so (.pyd on Windows) which we can import into our Python modules. .pxd files hold Cython definitions which can then be imported into .pyx files.

For this example, the extension.pxd will hold the function definition we will import into our interface.pyx file. These files can share the same name but have been named differently to avoid confusion.

Setup

On Windows you will need to install Visual Studio Installer from here. You can download the Community version. Once installed, check the Desktop development with C++ option from the Workloads tab and hit the Install while downloading button.

Visual studio download

For Linux, it will depend on the distribution you are using. For Ubuntu, run the following commands in the terminal:


apt update
apt install python3 python3-venv python3-dev gcc build-essential

With the required packages installed and the example_cython_binding repository cloned, you can create a virtual environment with Cython and setuptools. Run the following commands from within the example_cython_bindings folder:


Windows: python -m venv venv | Linux: python3 -m venv venv
Windows: venv\Scripts\activate | Linux: source /venv/bin/activate
python -m pip install Cython
python -m pip install setuptools

Later we will use setuptools along with Cython to build our Python binding.

Adding our c++ code

To start writing our Python binding, we first need C++ code to bind to! For this example, we will define a simple function called get_hello_world that returns a string.

In the \src\c_code.h file, we add the following lines:

#include <string>

std::string get_hello_world();

We still have to define what the function will do, so in the \src\c_code.cpp file we add the complete function definition. In this case, it will return Hello world!:

#include <string>
#include "c_code.h"

std::string get_hello_world()
{
    return "Hello world!";
}

Writing the Cython binding

Now that we have our C++ code written, we can start creating our Cython binding.

In the extension.pxd file we will add our Cython definition of our get_hello_world function. To do this, we tell Cython that the function definition is from an external file. In that case, the extension.pxd file should look like this:


from libcpp.string cimport string

cdef extern from "../src/c_code.h":
    string get_hello_world()

cdef extern from "../src/c_code.cpp":
    pass

Note, that the /src/c_code.cpp is also included. This is because Cython will not automatically look for the corresponding *.cpp for the *.h file. If we do not include it, the Python binding will not compile.

Now, in our interface.pyx we create our interface which will be called by Python. We import the extension.pxd file similar to how you would import a Python module and the result of our get_hello_world function is returned.


cimport extension


def get_hello_world():
    """ Return the character array from C as bytes.
    """
    return extension.get_hello_world()

The paths declared in the extension.pxd file are relative to the interface.pyx because this is the file that will be compiled.

Compiling the binding

Before we can compile the binding we will need to create a setup.py file with the following:


import os
from setuptools import setup, Extension
from Cython.Build import cythonize


ext_path = os.path.abspath(
    os.path.normpath("./cython/interface.pyx")
)

ext_modules = [
    Extension(
        "cython_extension",
        [ext_path],
        language="c++",
    ),
]

setup(
    name="cython_extension",
    ext_modules=cythonize(ext_modules),
)

The setup.py file is used to compile our Cython binding. Note that the name value is the name of the created module from which our code will be imported. We must also pass c++ as the language to the Extension instance, otherwise it will be compiled to c by default.

To compile the Cython binding, we must run the setup.py file using our virtual environment. To do this, run the following commands in the terminal with the directory set to the example_cython_bindings folder. The --inplace option will create the binary file in the current directory.


cd path\to\example_cython_bindings
venv\Scripts\activate
python setup.py build_ext --inplace

Testing the interface

With the Cython binding written and compiled, we can now test it! The using_interface.py file can be used to test the Python interface. Note, that it is in the same directory as the created binary.

We should be able to import our created module and call our function:


from cython_extension import get_hello_world
# print our result from c++
print(get_hello_world())


python -m using_interface.py

To store the created binary in a different directory, it will need to be in the PYTHONPATH environment variable.

Final words

Congratulations on creating your first Python binding using Cython! Hopefully, this article provided you with the tools to get started using Cython. Be sure to check out the finished code at the finished_example branch of the example_cython_binding repository.

If this article has piqued your interest, be sure to take a look through the Cython documentation.

Thank you to Bert Plasschaert and Kevin Chi Yan Tang for helping me test the example steps and providing feedback!