Getting started#
If you’ve never made a Python package before, packaging.python.org’s tutorial is a great place to start. It walks you through creating a simple package in pure Python using modern tooling and configuration. Another great resource is the Scientific Python Developer Guide.
Writing an extension#
We will be writing these files:
example-project
├── example.cpp
├── pyproject.toml
└── CMakeLists.txt
example-project
├── example.cpp
├── pyproject.toml
└── CMakeLists.txt
example-project
├── example.c
├── example.i
├── pyproject.toml
└── CMakeLists.txt
example-project
├── example.pyx
├── pyproject.toml
└── CMakeLists.txt
example-project
├── example.c
├── pyproject.toml
└── CMakeLists.txt
example-project
├── example.c
├── pyproject.toml
└── CMakeLists.txt
example-project
├── example.f
├── pyproject.toml
└── CMakeLists.txt
Source code#
For this tutorial, you can either write a C extension yourself, or you can use pybind11 and C++. Select your preferred version using the tabs - compare them!
#include <pybind11/pybind11.h>
namespace py = pybind11;
float square(float x) { return x * x; }
PYBIND11_MODULE(example, m) {
m.def("square", &square);
}
#include <nanobind/nanobind.h>
namespace nb = nanobind;
float square(float x) { return x * x; }
NB_MODULE(example, m) {
m.def("square", &square);
}
float square(float x) { return x * x; }
%module example
%{
/* Put header files here or function declarations like below */
extern float square(float x);
%}
extern float square(float x);
# cython: language_level=3
def square(float x):
return x * x
#define PY_SSIZE_T_CLEAN
#include <Python.h>
float square(float x) { return x * x; }
static PyObject *square_wrapper(PyObject *self, PyObject *args) {
float input, result;
if (!PyArg_ParseTuple(args, "f", &input)) {
return NULL;
}
result = square(input);
return PyFloat_FromDouble(result);
}
static PyMethodDef example_methods[] = {
{"square", square_wrapper, METH_VARARGS, "Square function"},
{NULL, NULL, 0, NULL}};
static struct PyModuleDef example_module = {PyModuleDef_HEAD_INIT, "example",
NULL, -1, example_methods};
/* name here must match extension name, with PyInit_ prefix */
PyMODINIT_FUNC PyInit_example(void) {
return PyModule_Create(&example_module);
}
#define PY_SSIZE_T_CLEAN
#include <Python.h>
float square(float x) { return x * x; }
static PyObject *square_wrapper(PyObject *self, PyObject *args) {
float input, result;
if (!PyArg_ParseTuple(args, "f", &input)) {
return NULL;
}
result = square(input);
return PyFloat_FromDouble(result);
}
static PyMethodDef example_methods[] = {
{"square", square_wrapper, METH_VARARGS, "Square function"},
{NULL, NULL, 0, NULL}};
static struct PyModuleDef example_module = {PyModuleDef_HEAD_INIT, "example",
NULL, -1, example_methods};
/* name here must match extension name, with PyInit_ prefix */
PyMODINIT_FUNC PyInit_example(void) {
return PyModule_Create(&example_module);
}
C FILE: EXAMPLE.F
SUBROUTINE SQUARE(B, X)
REAL B
REAL X
Cf2py intent(in) x
Cf2py intent(out) b
B = X * X
END
C END FILE EXAMPLE.F
Python package configuration#
To create your first compiled package, start with a pyproject.toml like this:
[build-system]
requires = ["scikit-build-core", "pybind11"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[build-system]
requires = ["scikit-build-core", "nanobind"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[build-system]
requires = ["scikit-build-core", "swig"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[build-system]
requires = ["scikit-build-core", "cython"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[tool.scikit-build-core]
wheel.py-api = "cp37"
[build-system]
requires = ["scikit-build-core", "numpy"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
dependencies = ["numpy"]
[tool.scikit-build]
ninja.minimum-version = "1.10"
cmake.minimum-version = "3.17.2"
Warning
The module you build will require an equal or newer version to the version of
NumPy it built with. You should use oldest-supported-numpy
or manually set
the NumPy version, though you will then be stuck with older versions of f2py.
Also it’s hard to compile Fortran on Windows as it’s not supported by MSVC and
macOS as it’s not supported by Clang.
Notice that you do not include cmake
, ninja
, setuptools
, or wheel
in
the requires list. Scikit-build-core will intelligently decide whether it needs
cmake
and/or ninja
based on what versions are present in the environment -
some environments can’t install the Python versions of CMake and Ninja, like
Android, FreeBSD, WebAssembly, and ClearLinux, but they may already have these
tools installed. Setuptools is not used by scikit-build-core’s native builder,
and wheel should never be in this list.
There are other keys you should include under [project]
if you plan to publish
a package, but this is enough to start for now. The
project metadata specification
page covers what keys are available. Another example is available at
the Scientific Python Library Development Guide.
CMake file#
Now, you’ll need a file called CMakeLists.txt
. This one will do:
cmake_minimum_required(VERSION 3.15...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX)
set(PYBIND11_NEWPYTHON ON)
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(example example.cpp)
install(TARGETS example LIBRARY DESTINATION .)
Scikit-build requires CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
If you place find Python first, pybind11 will respect it instead of the classic
FindPythonInterp/FindPythonLibs mechanisms, which work, but are not as modern.
Here we set PYBIND11_NEWPYTHON
to ON
instead of doing the find Python
ourselves. Pybind11 places its config file such that CMake can find it from
site-packages.
You can either use pybind11_add_module
or python_add_library
and then link
to pybind11::module
, your choice.
cmake_minimum_required(VERSION 3.15...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX)
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module)
find_package(nanobind CONFIG REQUIRED)
nanobind_add_module(example NB_STATIC example.cpp)
install(TARGETS example LIBRARY DESTINATION .)
Scikit-build and nanobind require CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
Nanobind places its config file such that CMake can find it from site-packages.
cmake_minimum_required(VERSION 3.15...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
find_package(
SWIG
COMPONENTS python
REQUIRED)
include(UseSWIG)
swig_add_library(
example
LANGUAGE python OUTPUT_DIR "${SKBUILD_PLATLIB_DIR}"
SOURCES example.i example.c)
if(WIN32)
set_property(TARGET example PROPERTY SUFFIX ".${Python_SOABI}.pyd")
else()
set_property(TARGET example
PROPERTY SUFFIX ".${Python_SOABI}${CMAKE_SHARED_MODULE_SUFFIX}")
endif()
target_link_libraries(example PRIVATE Python::Module)
install(TARGETS example DESTINATION .)
Scikit-build requires CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
You’ll need to handle the generation of files by SWIG directly.
cmake_minimum_required(VERSION 3.15...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
find_program(CYTHON "cython")
add_custom_command(
OUTPUT example.c
DEPENDS example.pyx
VERBATIM
COMMAND "${CYTHON}" "${CMAKE_CURRENT_SOURCE_DIR}/example.pyx" --output-file
"${CMAKE_CURRENT_BINARY_DIR}/example.c")
python_add_library(example MODULE "${CMAKE_CURRENT_BINARY_DIR}/example.c"
WITH_SOABI)
install(TARGETS example DESTINATION .)
Scikit-build requires CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
You’ll need to handle the generation of files by Cython directly at the moment. A helper (similar to scikit-build classic) might be added in the future.
cmake_minimum_required(VERSION 3.15...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
python_add_library(example MODULE example.c WITH_SOABI)
install(TARGETS example DESTINATION .)
Scikit-build requires CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
find_package(Python ...)
should always include the Development.Module
component instead of Developement
; the latter breaks if the embedding
components are missing, such as when you are building redistributable wheels on
Linux.
You’ll want WITH_SOABI
when you make the module to ensure the full extension
is included on Unix systems (PyPy won’t even be able to open the extension
without it).
cmake_minimum_required(VERSION 3.15...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.SABIModule
REQUIRED)
python_add_library(example MODULE example.c WITH_SOABI USE_SABI 3.7)
install(TARGETS example DESTINATION .)
Scikit-build requires CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
find_package(Python ...)
needs Development.SABIModule
for ABI3 extensions.
You’ll want WITH_SOABI
when you make the module. You’ll also need to set the USE_SABI
argument to the minimum version to build with. This will also add a proper
PRIVATE define of Py_LIMITED_API
for you.
Note
This will not support pypy, so you’ll want to provide an alternative if you support PyPy).
cmake_minimum_required(VERSION 3.17.2...3.26)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C Fortran)
find_package(
Python
COMPONENTS Interpreter Development.Module NumPy
REQUIRED)
# F2PY headers
execute_process(
COMMAND "${PYTHON_EXECUTABLE}" -c
"import numpy.f2py; print(numpy.f2py.get_include())"
OUTPUT_VARIABLE F2PY_INCLUDE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
add_library(fortranobject OBJECT "${F2PY_INCLUDE_DIR}/fortranobject.c")
target_link_libraries(fortranobject PUBLIC Python::NumPy)
target_include_directories(fortranobject PUBLIC "${F2PY_INCLUDE_DIR}")
set_property(TARGET fortranobject PROPERTY POSITION_INDEPENDENT_CODE ON)
add_custom_command(
OUTPUT examplemodule.c example-f2pywrappers.f
DEPENDS example.f
VERBATIM
COMMAND "${Python_EXECUTABLE}" -m numpy.f2py
"${CMAKE_CURRENT_SOURCE_DIR}/example.f" -m example --lower)
python_add_library(example MODULE "${CMAKE_CURRENT_BINARY_DIR}/examplemodule.c"
"${CMAKE_CURRENT_SOURCE_DIR}/example.f" WITH_SOABI)
target_link_libraries(example PRIVATE fortranobject)
install(TARGETS example DESTINATION .)
Scikit-build requires CMake 3.15, so there’s no need to set it lower than 3.15.
The project line can optionally use SKBUILD_PROJECT_NAME
and
SKBUILD_PROJECT_VERSION
variables to avoid repeating this information from
your pyproject.toml
. You should specify exactly what language you use to keep
CMake from searching for both C
and CXX
compilers (the default).
You’ll need to handle the generation of files by NumPy directly at the moment. A helper (similar to scikti-build classic) might be added in the future.
Finally, you install your module. The default install path will go directly to
site-packages
, so if you are creating anything other than a single
c-extension, you will want to install to the package directory (possibly
${SKBUILD_PROJECT_NAME}
) instead.
Building and installing#
That’s it! You can try building it:
$ pipx run build
pipx allows you to install and run Python applications in isolated environments.
Or installing it (in a virtualenv, ideally):
$ pip install .
That’s it for a basic package!
Other examples#
You can find other examples here:
pybind11’s scikit_build_core example: An example project using pybind11.
nanobind example: An example project using nanobind and the Stable ABI on Python 3.12+!
scikit-build-example-projects: Some example projects for both scikit-build and scikit-build-core.
Scientific-Python Cookie: A cookiecutter with 12 backends, including scikit-build-core.