Sitemap
Bug Theory

Bug Theory

Slow Python? Meet pybind11 !

6 min readMar 29, 2026

--

It started with a simple curiosity…

While exploring automatic differentiation (autograd) in modern Deep Learning frameworks, we wanted to understand how gradients are computed behind the scenes.

Tell me, I may forget..
Show me, I may remember..
Let me implement, I truly learn !

Per the saying, We tried implementing Andrej Karpathy’s micrograd, a tiny autograd engine written in pure Python built for educational purpose which explains minimal functions of autograd in about 50–100 lines of code.

After implementation, arose a natural question..

If Python is slow, how are Deep Learning frameworks so fast ??

This led us down another rabbit hole. Digging further had us discovering that many high-performance Python libraries rely heavily on C++ under the hood, with Python acting as a clean and flexible interface layer. This led us to explore how Python and C++ can work together and that’s when we encountered pybind11 !

What is pybind11 ?

pybind11 is a lightweight, header-only C++ library that enables seamless interoperability between C++ and Python. It allows developers to expose C++ classes, functions, and data structures directly to Python — without writing complex boilerplate code

In Nutshell: pybind11 lets you write performance-critical code in C++ and use it like normal Python code.

Press enter or click to view image in full size

Why pybind11 ?

Before pybind11,

  • Integrating Python with C++ meant dealing with the low-level Python/C API, manual memory management, and large amounts of boilerplate code.

After pybind11,

  • Modern C++11 features like templates, type inference, and smart pointers to automatically handle bindings and memory safely.

The result is clean, minimal, and Pythonic code that makes connecting C++ performance with Python usability significantly easier.

A Tiny example

With pybind11, exposing a simple C++ function to Python can be as small as:

int add(int i, int j) {
return i + j;
}

Binding it with pybind11:

PYBIND11_MODULE(example, m) {
m.def("add", &add, "A function that adds two numbers");
}

Now in Python:

import example
example.add(2, 3)

Where is pybind11 Used?

pybind11 is widely used in performance-critical Python libraries and research projects. Some notable users include

  • PyTorch
  • TensorFlow
  • ONNX Runtime
  • Open3D

This reinforces an important idea:

Many “Python” libraries are actually C++ engines with Python interfaces.

Core Concepts of pybind11

1.Module Binding — Create a Python module from C++ code

PYBIND11_MODULE(module_name, m)

2. Function Binding — Expose C++ functions

m.def("function_name", &function);

3. Class Binding — Expose C++ classes

py::class_<ClassName>(m, "ClassName")

4. Type Conversion — Automatic conversion between:

  • Python lists ↔ C++ vectors
  • Python ints ↔ C++ ints
  • NumPy arrays ↔ C++ buffers

This is one of pybind11’s most powerful features

Get Bug Theory’s stories in your inbox

Join Medium for free to get updates from this writer.

With these simple concepts, we can bridge Python and C++ seamlessly.
And that’s exactly what we did next !

We implemented micrograd in C++ and exposed it to Python using pybind11 !!

Let’s dive in..

Implementing Micrograd in C++ with pybind11

To explore Python and C++ interoperability, we re-implemented micrograd in C++ and exposed it to Python using pybind11. Micrograd itself is a tiny autograd engine (~100 lines) designed for educational purposes, making it ideal for experimentation.

Our implementation:
Github: https://github.com/kavinkumar807/micrograd_pybind_cpp

Project Structure:

value.h          # Core C++ Value struct with forward ops and backward pass
bindings.cpp # pybind11 bindings exposing Value to Python
setup.py # Build script (distutils/setuptools extension)
requirements.txt # Python dependencies (pybind11, setuptools)
backprop_test.py # Example usage / sanity check

Step 1: C++ Implementation (value.h)

We implemented the core Value class in C++ similar to Python micrograd

struct Value : public std::enable_shared_from_this<Value> {
double data;
double grad;

std::vector<std::shared_ptr<Value>> prev;
std::function<void()> _backward;

Value(double data) : data(data), grad(0.0) {}

// addition
std::shared_ptr<Value> add(std::shared_ptr<Value> other) {
auto out = std::make_shared<Value>(this->data + other->data);

out->prev = {shared_from_this(), other};

out->_backward = [self=shared_from_this(), other, out]() {
self->grad += 1.0 * out->grad;
other->grad += 1.0 * out->grad;
};

return out;
}

// multiplication
std::shared_ptr<Value> mul(std::shared_ptr<Value> other) {
auto out = std::make_shared<Value>(this->data * other->data);

out->prev = {shared_from_this(), other};

out->_backward = [self=shared_from_this(), other, out]() {
self->grad += other->data * out->grad;
other->grad += self->data * out->grad;
};

return out;
}
}

The below (bindings.cpp) exposes the Value class directly to Python.

PYBIND11_MODULE(micrograd_cpp, m) {
py::class_<Value, std::shared_ptr<Value>>(m, "Value")
.def(py::init<double>())
.def_readwrite("data", &Value::data)
.def_readwrite("grad", &Value::grad)
.def("backward", &Value::backward)

// methods
.def("add", &Value::add)
.def("mul", &Value::mul)

// python operators
.def("__add__", &Value::add)
.def("__mul__", &Value::mul);
}

Step 2: setup.py

We use setup.py to compile the C++ code into a Python extension module (.so file)

from setuptools import setup, Extension
import pybind11

ext_modules = [
Extension(
"micrograd_cpp",
["bindings.cpp"],
include_dirs=[pybind11.get_include()],
language="c++",
),
]

setup(
name="micrograd_cpp",
version="0.1",
ext_modules=ext_modules,
)

pybind11 integrates directly with setuptools, making compilation straightforward.

Step 3: Install Dependencies

pip install pybind11

Step 4: Build .so File

Generate the shared object file

python setup.py build_ext --inplace

This generates,

micrograd_cpp.cpython-*.so

This .so file is the compiled C++ module that Python can import.

Step 5: Using in Python

Now import like a normal Python module

import micrograd_cpp as mg

a = mg.Value(2.0)
b = mg.Value(3.0)

c = a * b + a # c = a*b + a => c = 8.0

c.backward()

print(a.grad) # 4.0 (dc/da = b + 1 = 3 + 1)
print(b.grad) # 2.0 (dc/db = a = 2)

That’s it !! Python calling C++ seamlessly !

What Just Happened?

  • C++ code compiled to .so
  • pybind11 exposed classes/functions
  • Python imported compiled module
  • C++ executed under the hood

Python simplicity; C++ performance;

Final Thoughts

What started as curiosity about autograd led us to explore something deeper: how Python leverages C++ for performance.

By re-implementing micrograd in C++ and exposing it with pybind11, we recreated a simplified version of the architecture used in many high-performance Python libraries.

PS: It’s also worth noting that pybind11 is not the only approach for language bindings. There are several alternatives such as SWIG, Boost.Python, Cython, and the native Python/C API — each with its own trade-offs and use cases.

< Not AI Generated :P >
Authors: Kavinkumar Baskar, Kavyapriya Govindarajan

References

Press enter or click to view image in full size
PFB links that led us down this rabbit hole..

pybind11

https://medium.com/@manish.surya.r/a-minimal-pybind11-example-calling-c-from-python-61352b8306da

https://medium.com/@ahmedfgad/pybind11-tutorial-binding-c-code-to-python-337da23685dc

https://medium.com/@stevechange/using-python-with-c-part1-3e894912214d

https://stackoverflow.com/questions/47599162/pybind11-how-to-package-c-and-python-code-into-a-singl…

https://dev.to/baduit/create-python-bindings-for-my-c-code-with-pybind11-2pck

https://medium.com/@qidi.liu.chn/how-to-use-pybind11-19aeaf4efbc1

https://youtu.be/_5T70cAXDJ0?si=TUSoRmWtg8vWj-Dy

https://youtu.be/_5T70cAXDJ0?si=7EiE-WoDuEG_mpkJ

https://pybind11.readthedocs.io/en/stable/

Python/C API/Cython

https://medium.com/@wansac/the-python-c-api-a-brief-introduction-7926ea0ef488

https://medium.com/@elniak/integrate-c-with-python-a-comprehensive-guide-for-developers-e98a1883fc7b

https://docs.python.org/3/c-api/index.html

https://medium.com/@iftimiealexandru/bridging-the-gap-between-python-and-c-c-libraries-an-explorati…

https://medium.com/@tate.wilks/writing-c-extensions-for-python-38aa0e58bf01

SWIG

https://medium.com/@howard03405vis/beginning-swig-compiler-f5c32f1c9266

https://medium.com/@som.mukhopadhyay/do-you-want-your-python-code-to-access-c-c-code-use-swig-bc972…

https://byteblog.medium.com/swig-calling-c-code-from-python-9d90ff7c9759

Boost

https://www.boost.org/doc/libs/latest/libs/python/doc/html/article.html

https://medium.com/@x10tion007/why-you-should-boost-python-with-c-wrappers-unlock-speed-and-product…

--

--