Skip to content

conan-py-build

A build backend to create Python wheels with C/C++ extensions using Conan.


Install

pip install conan-py-build

Getting started

Minimal project: a Python package with a C++ extension that uses a Conan dependency.

Project layout

mypackage/
├── pyproject.toml
├── conanfile.py
├── CMakeLists.txt
└── src/
    ├── mymodule.cpp
    └── mypackage/
        └── __init__.py

pyproject.toml

[build-system]
requires = ["conan-py-build"]
build-backend = "conan_py_build.build"

[project]
name = "mypackage"
version = "0.1.0"

conanfile.py

from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout

class MyPackageConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeToolchain", "CMakeDeps"

    def layout(self):
        cmake_layout(self)

    def requirements(self):
        self.requires("fmt/12.1.0")

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        cmake = CMake(self)
        cmake.install()

CMakeLists.txt

The backend takes everything that cmake --install produces and stages it into the wheel. For the extension to be importable, the DESTINATION in install() must match your Python package name — that is how the compiled .so / .pyd ends up next to __init__.py:

cmake_minimum_required(VERSION 3.15)
project(mypackage LANGUAGES CXX)

find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module)
find_package(fmt REQUIRED)

Python3_add_library(_core MODULE src/mymodule.cpp)
target_link_libraries(_core PRIVATE fmt::fmt)

install(TARGETS _core DESTINATION mypackage)

The resulting wheel layout:

mypackage/
├── __init__.py   ← from src/mypackage/
└── _core.so      ← from install(TARGETS _core DESTINATION mypackage)

If DESTINATION doesn't match (e.g. lib instead of mypackage), the extension lands outside the package and import mypackage._core will fail.

src/mymodule.cpp

This example uses the Python C API directly to keep it dependency-free. For real projects, pybind11 and nanobind are more ergonomic — see Examples.

#include <Python.h>
#include <fmt/core.h>
#include <string>

static PyObject* greet(PyObject* self, PyObject* args) {
    const char* name;
    if (!PyArg_ParseTuple(args, "s", &name)) return NULL;
    std::string msg = fmt::format("Hello, {}!", name);
    return PyUnicode_FromString(msg.c_str());
}

static PyMethodDef methods[] = {
    {"greet", greet, METH_VARARGS, "Greet someone."},
    {NULL, NULL, 0, NULL},
};

static struct PyModuleDef module = {
    PyModuleDef_HEAD_INIT, "mypackage._core", NULL, -1, methods,
};

PyMODINIT_FUNC PyInit__core(void) {
    return PyModule_Create(&module);
}

src/mypackage/__init__.py

from mypackage._core import greet

Build and test

pip wheel . -w dist/ -vvv
pip install dist/mypackage-0.1.0-*.whl
python -c "from mypackage import greet; greet('world')"

What's next?

Goal Page
All --config-settings and pyproject.toml options Configuration
pybind11, nanobind, cibuildwheel Examples
Contributing, tests, docs Development