Build a Python C/C++ extension using Conan and conan-py-buildο
In this example, we are going to create a small Python package with a C++ extension
that uses pybind11 to expose the C++ code to
Python and fmt as a regular C++ dependency. Both dependencies
are downloaded and built by Conan, driven by
conan-py-build, a
PEP 517 build backend that lets pip (or
build, or uv) build the C/C++ part of a Python package through Conan.
Please, first clone the sources to recreate this project. You can find them in the examples2 repository in GitHub:
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/examples/extensions/python_build_backend
The project has this structure:
.
βββ pyproject.toml
βββ conanfile.py
βββ CMakeLists.txt
βββ LICENSE
βββ src
β βββ myadder.cpp
βββ python
βββ myadder
βββ __init__.py
Letβs have a look at the pyproject.toml file:
[build-system]
requires = ["conan-py-build"]
build-backend = "conan_py_build.build"
[project]
name = "myadder"
version = "0.1.0"
description = "A simple Python package with a C++ extension"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.8"
[tool.conan-py-build.wheel]
packages = ["python/myadder"]
Setting conan-py-build as the build-backend means that every time this
package is built (pip wheel ., pip install ., python -m build, etc.), the
backend will read the recipe declared in the standard conanfile.py, resolve and
install its dependencies through Conan, run the build() and package() methods,
and copy everything package() staged into the resulting wheel.
The [tool.conan-py-build.wheel] section tells the backend where the pure Python
part of the package lives, so it also gets copied into the wheel next to the compiled
extension. See the configuration reference
for all the available options (dynamic versioning, extra Conan profiles/arguments,
entry points, sdist contents, etc.).
Now, the conanfile.py is a regular Conan recipe, no different from one used to create a C++ package:
import sys
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
class MyAdderConan(ConanFile):
name = "myadder"
settings = "os", "compiler", "build_type", "arch"
def layout(self):
cmake_layout(self)
def requirements(self):
self.requires("pybind11/3.0.1")
self.requires("fmt/12.1.0")
def generate(self):
tc = CMakeToolchain(self)
tc.cache_variables["Python3_EXECUTABLE"] = sys.executable
tc.generate()
deps = CMakeDeps(self)
deps.generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
It declares pybind11 and fmt as regular requirements, downloaded as precompiled binaries from ConanCenter (or built from source if needed), and uses CMakeToolchain and CMakeDeps to configure and build the extension with CMake.
Important
Setting Python3_EXECUTABLE to sys.executable keeps CMake on the
same interpreter pip uses, avoiding a wrong ABI build.
Important
For the compiled extension to be importable, it must land under a directory
that matches the Python package name, so the .so/.pyd ends up next to
__init__.py in the wheel. This is controlled by the DESTINATION argument
of CMakeβs install(TARGETS ...).
The CMakeLists.txt builds the _core extension module with pybind11, links it
against fmt, and installs it into a myadder folder to match the Python
package name:
cmake_minimum_required(VERSION 3.15)
project(myadder LANGUAGES CXX)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 CONFIG REQUIRED)
find_package(fmt REQUIRED)
pybind11_add_module(_core src/myadder.cpp)
target_link_libraries(_core PRIVATE pybind11::module fmt::fmt)
install(TARGETS _core DESTINATION myadder)
The C++ source exposes an add() function to Python, using fmt to print
the result in bold green text:
#include <pybind11/pybind11.h>
#include <fmt/core.h>
#include <fmt/color.h>
namespace py = pybind11;
double add(double a, double b) {
double result = a + b;
fmt::print(fg(fmt::color::green) | fmt::emphasis::bold,
"{} + {} = {}\n", a, b, result);
return result;
}
PYBIND11_MODULE(_core, m) {
m.doc() = "Simple Python extension using fmt via Conan.";
m.def("add", &add, "Add two numbers and print the result formatted with fmt.",
py::arg("a"), py::arg("b"));
}
And finally, python/myadder/__init__.py just re-exports the function from the compiled module:
from myadder._core import add
__all__ = ["add"]
Build and testο
Build the wheel with pip. Thereβs no need to install conan-py-build
beforehand: since itβs declared under [build-system] requires in
pyproject.toml, pip installs it (and Conan itself) into an isolated
build environment automatically:
$ pip wheel . -w dist/
Conan will install pybind11 and fmt, build the extension with CMake, and
conan-py-build will assemble the resulting wheel and save it into the dist
folder, e.g. dist/myadder-0.1.0-cp312-cp312-macosx_14_0_arm64.whl (the exact
filename depends on your platform and Python version).
A wheel is just a zip file, so you can check what got packaged without installing it:
$ python -m zipfile -l dist/myadder-*.whl
File Name Modified Size
myadder/__init__.py 2026-07-08 14:01:30 49
myadder/_core.cpython-312-darwin.so 2026-07-08 14:41:12 233288
myadder-0.1.0.dist-info/METADATA 2026-07-08 14:41:08 212
myadder-0.1.0.dist-info/WHEEL 2026-07-08 14:41:12 101
myadder-0.1.0.dist-info/licenses/LICENSE 2026-07-08 14:01:32 1083
myadder-0.1.0.dist-info/RECORD 2026-07-08 14:41:12 471
Note how the compiled _core extension, the one Conanβs package() staged
during the build, ends up right next to __init__.py under myadder/ β
exactly the layout the DESTINATION in CMakeLists.txt was set up for.
Now install it and try it out:
$ pip install dist/myadder-*.whl
$ python -c "from myadder import add; add(2, 3)"
2 + 3 = 5
The line above is printed by the C++ extension in bold green, courtesy of fmt β a visible sign that the compiled code, not Python, produced it.
See also
conan-py-build documentation, for configuration options like dynamic versioning, custom Conan profiles, entry points, and shared library handling with
auditwheel/delocate/delvewheel.Introducing conan-py-build blog post.
conan-py-build examples, including Meson, nanobind, and cibuildwheel-based builds.