Thursday, February 5, 2026

nanobind: the bridge between C++ and Python...

Python is where ideas move fast. C++ is where performance lives. For years, gluing the two together meant choosing between power, safety, and sanity. Enter nanobind—a modern, minimalist bridge that lets C++ and Python talk to each other cleanly, efficiently, and without ceremony.

nanobind is a C++17-first binding library, inspired by pybind11 but redesigned with a sharper focus on performance, compile times, and modern C++ practices. It’s built for developers who care about control—over lifetimes, memory, ABI stability, and error handling—without drowning in boilerplate.

What makes nanobind special is how unapologetically low-level it is, while still feeling elegant. It avoids heavy template metaprogramming where possible, compiles fast, and produces smaller binaries. If you’re working on large C++ codebases—CAD kernels, physics engines, solvers, graphics pipelines—this matters a lot.

Memory management is another strong point. nanobind gives you explicit control over object ownership between C++ and Python, making it ideal for systems where Python is embedded inside a C++ application (think Blender, FreeCAD, game engines, or simulation tools). You’re not just exporting functions—you’re designing an API boundary.

Compared to older tools like SWIG, nanobind doesn’t try to “auto-magically” generate bindings. That’s a feature, not a bug. You write bindings intentionally, so the Python API feels Pythonic, not like a leaked C++ header. And compared to pybind11, nanobind is leaner, stricter, and more future-facing.

In short:

  • Python for orchestration and scripting

  • C++ for performance-critical logic

  • nanobind as the clean, modern handshake between them

If you’re building serious software where Python is a first-class citizen—but not the performance bottleneck—nanobind is one of the best bridges you can choose today.

Here's my today's exploration on nanobind - the Factory pattern written in C++ being given a python interface using nanobind.

Enjoy...

The Factory Pattern written in C++.

/*

* Food.h

*

* Created on: Mar 10, 2021

* Author: som

*/


#ifndef FOOD_H_

#define FOOD_H_

#include <string>


using namespace std;


class Food {

public:

virtual string getName() = 0;


virtual ~Food(){


}

};



#endif /* FOOD_H_ */


/*

* Biscuit.h

*

* Created on: Mar 10, 2021

* Author: som

*/


#ifndef BISCUIT_H_

#define BISCUIT_H_


#include "Food.h"


class Biscuit: public Food {

public:

Biscuit();

string getName();

~Biscuit();

};


#endif /* BISCUIT_H_ */


/*

* Biscuit.cpp

*

* Created on: Mar 10, 2021

* Author: som

*/

#include <iostream>

#include "Biscuit.h"

using namespace std;



Biscuit::Biscuit() {

// TODO Auto-generated constructor stub

cout<<"Biscuit is made..."<<endl;


}


Biscuit::~Biscuit(){}



string Biscuit::getName(){

return "It's a Biscuit";

}



/*

* Chocolate.h

*

* Created on: Mar 10, 2021

* Author: som

*/


#ifndef CHOCOLATE_H_

#define CHOCOLATE_H_


#include <iostream>

#include "Food.h"


class Chocolate: public Food {

public:

Chocolate();

virtual ~Chocolate();

string getName();

};



#endif /* CHOCOLATE_H_ */




/*

* Chocolate.cpp

*

* Created on: Mar 10, 2021

* Author: som

*/


#include "Chocolate.h"



Chocolate::Chocolate() {

// TODO Auto-generated constructor stub

cout<<"Chocolate is made..."<<endl;


}


Chocolate::~Chocolate() {

// TODO Auto-generated destructor stub

}


string Chocolate::getName(){

return "It's a Chocolate";

}



/*

* Factory.h

*

* Created on: Mar 10, 2021

* Author: som

*/


#ifndef FACTORY_H_

#define FACTORY_H_


#include <nanobind/nanobind.h>

#include <iostream>

#include <string>


#include "Biscuit.h"

#include "Chocolate.h"


using namespace std;


class Factory{

public:

static Factory* instance;

static Factory* getInstance();


Food* makeFood(const string& type);


private:

Factory(){}


// Delete copy constructor & assignment operator (Singleton pattern)

Factory(const Factory&) = delete;

Factory& operator=(const Factory&) = delete;

};

//Factory* Factory:: instance = NULL;



#endif /* FACTORY_H_ */



/*

* Factory.cpp

*

* Created on: Jan 30, 2025

* Author: som

*/

#include "Factory.h"

Factory* Factory::instance = NULL;


Factory* Factory:: getInstance(){

if(Factory::instance == NULL){

Factory::instance = new Factory();

}

return Factory::instance;

}


Food* Factory::makeFood(const string& type){

if(type.compare("bi") == 0){

return new Biscuit();

}

if(type.compare("ch") == 0){

return new Chocolate();

}


return NULL;

}



The bindings.cpp - defining the bridge...



#include <nanobind/nanobind.h>

#include <nanobind/stl/string.h> // Required for std::string support

#include "Factory.h"


namespace nb = nanobind;


// Use NB_MODULE to define the extension

NB_MODULE(foodfactory, m) {

// 1. Wrap the Base Class

// We use nb::class_<T> and provide the name it will have in Python

nb::class_<Food>(m, "Food")

.def("getName", &Food::getName);


// 2. Wrap the Subclasses

// Note: We specify the base class <Biscuit, Food> so Python knows the relationship

nb::class_<Biscuit, Food>(m, "Biscuit")

.def(nb::init<>());


nb::class_<Chocolate, Food>(m, "Chocolate")

.def(nb::init<>());


// 3. Wrap the Singleton Factory

nb::class_<Factory>(m, "Factory")

.def_static("get_instance", &Factory::getInstance, // Renamed to snake_case for Python idiomatic style

nb::rv_policy::reference)


.def("make_food", &Factory::makeFood, // Match your Python script's call

nb::rv_policy::take_ownership);}



And here is the CMakeLists.txt for compiling and creating the shared object


cmake_minimum_required(VERSION 3.15)

project(FactoryPattern_nanobind)


set(CMAKE_CXX_STANDARD 17)

set(CMAKE_CXX_STANDARD_REQUIRED ON)


# 1. Find Python

find_package(Python 3.9 COMPONENTS Interpreter Development REQUIRED)


# 2. Get nanobind paths manually to bypass the "Target Not Found" bug

execute_process(

COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir

OUTPUT_VARIABLE NB_DIR OUTPUT_STRIP_TRAILING_WHITESPACE

)

execute_process(

COMMAND "${Python_EXECUTABLE}" -m nanobind --include_dir

OUTPUT_VARIABLE NB_INC OUTPUT_STRIP_TRAILING_WHITESPACE

)


# 3. Load the nanobind logic

list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}")

find_package(nanobind REQUIRED CONFIG)


# 4. Create the module

nanobind_add_module(foodfactory

LTO

Biscuit.cpp

Chocolate.cpp

Factory.cpp

bindings.cpp

)


# 5. MANUALLY fix the missing target link

# This bypasses the error by providing the includes and libraries directly

target_include_directories(foodfactory PRIVATE "${NB_INC}" "${Python_INCLUDE_DIRS}")

target_link_libraries(foodfactory PRIVATE Python::Module)


# If nanobind still complains about the target, we define it as an alias

if(NOT TARGET nanobind::nanobind)

add_library(nanobind::nanobind INTERFACE IMPORTED)

set_target_properties(nanobind::nanobind PROPERTIES

INTERFACE_INCLUDE_DIRECTORIES "${NB_INC}"

)

endif()


After compiling and creating the shared object, you need to put it

inside the Python project and import it.


The Python code looks as follows.


import foodfactory
# Access get_instance THROUGH the Factory class
factory = foodfactory.Factory.get_instance()
biscuit = factory.make_food("bi")
print(biscuit.getName())
chocolate = factory.make_food("ch")
print(chocolate.getName())

And here are my earlier explorations on C++-Python bridge like pybind11, Boost.Python, SWIG, etc.

Enjoy