Tuesday, March 17, 2026

PyBind Setup Cheat Sheet

In 2024, we checked out OpenAI Retro Cheat Sheet as an open-source project that provides an interface to interact with various retro video games for purpose of Reinforcement Learning research. This project: using C/C++ for high-performance but leverages pybind11 to create bindings for Python client code consumption.

Let's check it out!

pybind11
A lightweight header-only library that can be used to integrate C++ with Python to create bindings exposing C++ functions to Python. Client code written in Python can be consumed to invoke the underlying C++ code.

Installation
All examples here are executed on Ubuntu Linux. Therefore install pybind11 globally to begin the examples:
 sudo apt-get update
 sudo apt-get install pybind11-dev
 sudo apt install build-essential g++

Example I
Create an example that exposes C++ function to Python with pybind11. Launch terminal | Enter commands:
  mkdir -p ~/HelloPyBind
  cd HelloPyBind
  python  -m venv .venv
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.8.10
  pip install pybind11
  pip install --upgrade pip


Create the following files: example.cpp, setup.py, test.py. Enter the following C++ and Python source code:
  example.cpp
  #include <pybind11/pybind11.h>
  
  int add(int x, int y)
  {
      return x + y;
  }
  
  PYBIND11_MODULE(example, m)
  {
      // optional module docstring
      m.doc() = "pybind11 example plugin";
      m.def("add", &add, "A function which adds two numbers");
  }

  setup.py
  from setuptools import setup, Extension
  import pybind11
  
  ext_modules = [
      Extension(
          "example",
          ["example.cpp"],
          include_dirs=[pybind11.get_include()],
          language="c++"
      ),
  ]
  
  setup(
      name="example",
      version="0.1",
      ext_modules=ext_modules,
  )

  test.py
  import example
  
  result = example.add(1, 2)
  print(f"1 + 2 = {result}")

Build C++ code using setup.py build inplace. Finally execute python test.py for Python to execute C++ code!
  python setup.py build_ext --inplace		# example.cpython-38-x86_64-linux-gnu.so
  python test.py				# OUTPUT	1 + 2 = 3


Example II
Repeat previous exercise but prefer PyCharm IDE. Launch PyCharm | New Project. Enter the following info:

 Location: ~/HelloPyBind
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
  uv venv --python 3.11
  source .venv/bin/activate           # OR .\.venv\Scripts\activate
  which python
  `which python` --version            # Python 3.11.11

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv add pybind11
  uv add setuptools
  uv lock
  uv sync

Create the following files: example.cpp, setup.py, test.py. Enter C++ and Python code similar to Example I. Build C++ code using setup.py build install. Finally execute uv run test.py for Python to execute C++ code!
  uv run setup.py build		
  uv run setup.py install		# example.cpython-38-x86_64-linux-gnu.so
  uv run test.py			# OUTPUT	3 + 5 = 8	9 - 5 = 4


Example III
Repeat previous exercise but prefer CMake to build C++ code via CMakeLists.txt. Create PyCharm Project:
 Location: ~/HelloPyBind
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
  uv add pybind11
  uv sync

Create the following files: example.cpp, CMakeLists.txt, test.py. Enter code similar to Example II but update:
  CMakeLists.txt
  cmake_minimum_required(VERSION 3.16)
  project(example)
  
  # Find the Python 3.11-specific pybind11 CONFIG from pip
  execute_process(
          COMMAND ${Python3_EXECUTABLE} -m pybind11 --cmakedir
          OUTPUT_VARIABLE pybind11_DIR
          OUTPUT_STRIP_TRAILING_WHITESPACE
  )
  find_package(pybind11 REQUIRED CONFIG)
  pybind11_add_module(example example.cpp)

Build C++ code using cmake and make. In the PyCharm Terminal | Enter the following commands to build:
  mkdir -p build
  cd build
  cmake -DPython3_EXECUTABLE=$(which python) ..
  make -j$(grep -c ^processor /proc/cpuinfo)

Enter commands to copy library to be used. Finally execute uv run test.py for Python to execute C++ code!
  python -c "import sysconfig, shutil, glob;		\		
  dst = sysconfig.get_paths()['platlib'];		\
  so = glob.glob('*.so')[0];				\
  shutil.copy2(so, dst)"
  cd ..
  uv run test.py			# OUTPUT	Hello, World!

IMPORTANT
The first 3x examples worked but tightly coupled Python and C++ without being able to debug separately!

Example IV
Repeat previous exercise but prefer to modify the project layout to separate top level Python and C++ code:
  ~/HelloPyBind/
  ├── cpp/
  │   ├── src/
  │   │   ├── api/
  │   │   │   ├── my_api.h
  │   │   │   └── my_api.cpp
  │   │   ├── bindings/
  │   │   │   └── pybind_module.cpp       # pybind11 bindings
  │   │   ├── CMakeLists.txt
  │   │   └── main.cpp                    # C++ executable entry point
  │   ├── tests/
  │   │   ├── CMakeLists.txt
  │   │   └── test_api.cpp
  │   └── CMakeLists.txt                  # top-level C++ (CLion entry point)
  │
  └── python/
      ├── .venv/
      │   └── lib/
      │       └── python3.11/
      │           └── site-packages/
      │               └── my_api_py.cpython-311-x86_64-linux-gnu.so
      ├── test.py
      ├── pyproject.toml
      └── README.md

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code.
 Location: ~/HelloPyBind/python
 Interpreter type:  uv
 Python version: 3.11
 Path to uv:  ~/.local/bin/uv

Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion information:
 C++ C++ Executable
 Location: ~/HelloPyBind/cpp
 Language standard: C++17

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory

Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

IMPORTANT
CMakeLists.txt files are configured to copy shared object SO file into the Python .venv virtual environment

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  uv run test.py				# OUTPUT	1 + 2 = 3


Example V
Repeat previous exercise but prefer more complexity to build C++ code with classes as consumed by Python

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code. Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion from before.

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory. Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  uv run test.py		# OUTPUT	
  # Guitar: 'Fender' [6-string] = $1500.0
  # Guitar: 'Ibanez' [7-string] = $1200.0
  # Guitar: 'Gibson' [6-string] = $2400.0


Example VI
Repeat previous exercise but prefer more complexity to build C++ code with templates consumed by Python

Create PyCharm Project. Setup virtual environment as before then create CLion project to build C++ code. Launch CLion | New Project. Create C++ Executable using C++ 17. Enter the following CLion from before.

Set build directory in CLion. File menu | Settings... | Build, Execution, Deployment | CMake | Build directory. Setup folder layout as above. Enter all C++ source code and tests. Rebuild entire solution in Debug mode.

Launch PyCharm | Complete the test runner. Finally execute uv run test.py for Python to execute C++ code!
  # OUTPUT	
  # Container[0] = 0.0
  # Container[1] = 1.0
  # Container[2] = 2.0
  # Container[3] = 3.0
  # Container[4] = 4.0
  # OUTPUT	
  # Container[5] = 5.0
  # Container[6] = 6.0
  # Container[7] = 7.0
  # Container[8] = 8.0
  # Container[9] = 9.0


Example VII
Repeat previous exercise but prefer Visual Studio 2022 on Windows to build an increasing C++ code base:
  ~/HelloPyBind/
  ├── cpp/
  │   ├── src/
  │   │   ├── core/			  # Core API implementation
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── math/			  # Math-related API
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── mesh/			  # Mesh-related API
  │   │   │   ├── *.h
  │   │   │   └── *.cpp
  │   │   ├── bindings/
  │   │   │   └── pybind_module.cpp       # pybind11 bindings
  │   │   ├── CMakeLists.txt
  │   │   └── main.cpp                    # C++ executable entry point
  │   ├── tests/
  │   │   ├── CMakeLists.txt
  │   │   ├── test_matrix.cpp
  │   │   ├── test_vector.cpp
  │   │   ├── test_mesh.cpp
  │   │   ├── test_mesh_algorithms.cpp
  │   │   └── test_mesh_processor.cpp
  │   └── CMakeLists.txt                  # top-level C++ (CLion entry point)
  └── python/

Launch Visual Studio 2022 | Continue without code. File | Open | CMake... Navigate to cpp/CMakeLists.txt. Build menu | Build All. Finally, choose Test menu | Test Explorer. Run All Tests in View or choose to Debug:


Summary
To summarize, we have demonstrated various PyBind examples in which C++ library code is consumed by a single Python API exclusively. However, in future there may be instances in which the C++ library may need to be consumed by multiple languages. In this case ctypes.cdll.LoadLibrary() may be better that PyBind!

No comments:

Post a Comment