Introducing conan-py-build: Build Python Wheels with Conan
Packaging Python extensions that contain native C or C++ code has come a long
way. PEP 517 defined a contract between
Python build frontends (pip, build, uv) and the build backend
that produces the wheel. That standard is what makes it possible today to
connect a CMakeLists.txt to a pyproject.toml, declare a backend, and let
pip wheel . drive the build.
The C/C++ dependency layer is a different story. Somewhere between
pyproject.toml and CMakeLists.txt, a find_package(OpenSSL) has to resolve.
In practice, most projects solve that outside the wheel build: through system
packages, vendored source trees, FetchContent or a separate native package
manager install step. That means a separate step to manage before the Python
build, often duplicated across CI configurations and developer setups.
Today, we are happy to introduce conan-py-build, a PEP 517 build backend that brings Conan’s C/C++ dependency management directly into the Python wheel build.
The project is currently in beta and under active development. We are releasing it now to gather early feedback, and we would love for you to try it and tell us what you think.
What is conan-py-build?
conan-py-build is a build backend for Python packages that contain native
C/C++ extensions. You declare it in pyproject.toml, provide a conanfile.py
that describes the C/C++ build and its dependencies, and build wheels through
standard Python packaging commands such as pip wheel ..
When a build runs, conan-py-build:
- Resolves the C/C++ dependency graph through Conan, downloading precompiled binaries where available and building the rest from source
- Prepares the build toolchain through the corresponding Conan generators
- Builds the extension using your project’s build system
- When the extension links against shared libraries, copies those runtime dependencies next to the extension module and patches RPATH on Linux and macOS where applicable
- Packages the result into a standard Python wheel
Because it is a PEP 517 backend, it plugs into pip, build, and uv
directly, and fits into cibuildwheel-based CI workflows for multi-platform
builds.
A minimal example
Let’s build a tiny Python package that exposes a single function, greet(name),
which prints a colored greeting to the terminal. We’ll use CMake for the native
build, pybind11 for the Python bindings,
and {fmt} as a dependency pulled in through Conan. The same
setup extends to other build systems like Meson or Autotools.
The project layout:
mypackage/
├── pyproject.toml
├── conanfile.py
├── CMakeLists.txt
└── src/
├── mypackage/
│ └── __init__.py
└── mypackage.cpp
pyproject.toml declares the build backend and the project metadata:
[build-system]
requires = ["conan-py-build"]
build-backend = "conan_py_build.build"
[project]
name = "mypackage"
version = "0.1.0"
conanfile.py describes the C/C++ side: its dependencies (pybind11 and fmt)
and how they are built and packaged.
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("pybind11/3.0.1")
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 builds the extension against pybind11 and fmt and installs
the resulting module into the Python package directory so the backend picks it
up when assembling the wheel:
cmake_minimum_required(VERSION 3.15)
project(mypackage LANGUAGES CXX)
set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 CONFIG REQUIRED)
find_package(fmt CONFIG REQUIRED)
pybind11_add_module(_core src/mypackage.cpp)
target_link_libraries(_core PRIVATE fmt::fmt)
install(TARGETS _core DESTINATION mypackage)
The C++ source defines greet(name) using fmt’s color support and exposes it
as a compiled _core module:
#include <string>
#include <pybind11/pybind11.h>
#include <fmt/color.h>
void greet(const std::string& name) {
fmt::print(fmt::fg(fmt::color::green), "Hello, {}!\n", name);
}
PYBIND11_MODULE(_core, m) {
m.def("greet", &greet);
}
And src/mypackage/__init__.py re-exports it so callers see mypackage.greet:
from mypackage._core import greet
__all__ = ["greet"]
With that in place, building the wheel is the standard Python packaging command:
$ pip wheel . -w dist/
Conan resolves pybind11 and fmt from Conan Center Index, CMake compiles the
extension against them, and you get a platform-specific wheel in dist/.
Install it and try it:
$ pip install dist/mypackage-*.whl
$ python -c "import mypackage; mypackage.greet('world')"
Hello, world!
You should see Hello, world! printed in green.
What conan-py-build brings
Some of the advantages of bringing Conan into the wheel build:
- One build entry point. The usual
pip wheel .command can drive both the Python packaging step and the native C/C++ dependency/build step. - Conan Center. A large catalog of C/C++ libraries with recipes tested across a broad compiler and OS matrix.
- Binary caching. Compiled dependencies are reused across builds and CI runs via the Conan cache or a shared remote, rebuilt only when settings change.
- Profiles and lockfiles. Profiles define the native build configuration of each wheel (compiler, architecture, C++ standard, dependency options), and lockfiles pin the graph for reproducible builds.
- Shared library handling. Conan-managed runtime libraries are deployed next to the extension module, and RPATH is adjusted on Linux and macOS where applicable.
Conclusions
conan-py-build pulls the C/C++ dependency layer inside the PEP 517 build so
the Python build and the C/C++ build are one problem, not two. If you have been
maintaining a separate dependency step alongside your Python packaging, it is
worth a look.
Check out the documentation and browse the
examples.
conan-py-build is still in beta and available on
PyPI. If something does not work, or
there is a workflow you want supported, please open an issue on
GitHub and let us know
where it fits, or where it does not yet.
Looking forward to your feedback.