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. And a tutorial can be found at INTERSECT Training: Packaging.
You can also find examples:
For pybind11, there’s a example template at pybind11/scikit_build_example. For nanobind, nanobind example includes the Stable ABI on Python 3.12+!
There are several examples including scikit-build-core examples (including free-threading) at scikit-build-sample-projects.
We also keep a list of some of the projects using scikit-build-core.
Quick start¶
There are several mechanisms to quickly get started with a package:
Scikit-build-core ships an
initcommand:scikit-build init --backend pybind11 myprojectscaffolds the exact project this page builds (run without--backendto pick one interactively). The rest of this page walks through what it generates.uv has built-in support for scikit-build-core. Just make a directory for your package and run:
uv init --lib --build-backend=scikit.scientific-python/cookie has a cookiecutter/copier template for making a package with all the suggestions in the Scientific Python Developer Guide.
buildgen can generate Python extensions with
buildgen new myext -r py/pybind11(see all withbuildgen list, includes pybind11, nanobind, Cython, and C extensions).
Added in version 1.0: The scikit-build init command.
Writing an extension¶
We will be writing these files:
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.cpp
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.cpp
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
├── _core.c
└── _core.i
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.pyx
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.c
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.c
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.c
example
├── pyproject.toml
├── CMakeLists.txt
└── src
└── example
├── __init__.py
└── _core.f
Source code¶
The compiled extension is built as _core and lives inside the example
package at src/example/_core.*. Select your binding tool using the tabs:
#include <pybind11/pybind11.h>
namespace py = pybind11;
float square(float x) { return x * x; }
PYBIND11_MODULE(_core, m) { m.def("square", &square); }
#include <nanobind/nanobind.h>
namespace nb = nanobind;
float square(float x) { return x * x; }
NB_MODULE(_core, m) { m.def("square", &square); }
float square(float x) { return x * x; }
%module _core
%{
/* 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 _core_methods[] = {
{"square", square_wrapper, METH_VARARGS, "Square function"},
{NULL, NULL, 0, NULL}};
static struct PyModuleDef _core_module = {PyModuleDef_HEAD_INIT, "_core", NULL,
-1, _core_methods};
/* name here must match extension name, with PyInit_ prefix */
PyMODINIT_FUNC PyInit__core(void) { return PyModule_Create(&_core_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 _core_methods[] = {
{"square", square_wrapper, METH_VARARGS, "Square function"},
{NULL, NULL, 0, NULL}};
static struct PyModuleDef _core_module = {PyModuleDef_HEAD_INIT, "_core", NULL,
-1, _core_methods};
/* name here must match extension name, with PyInit_ prefix */
PyMODINIT_FUNC PyInit__core(void) { return PyModule_Create(&_core_module); }
#define PY_SSIZE_T_CLEAN
#include <Python.h>
/* Free-threaded Stable ABI (abi3t, PEP 803) example. abi3t requires the
* PEP 793 module-export mechanism: with the opaque PyObject it enables, the
* classic static PyModuleDef cannot be used. abi3t is a subset of abi3, so a
* single build loads on both free-threaded and GIL-enabled CPython 3.15+. */
static 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 _core_methods[] = {
{"square", square_wrapper, METH_VARARGS, "Square function"},
{NULL, NULL, 0, NULL}};
PyABIInfo_VAR(_core_abi_info);
/* name here must match extension name, with PyModExport_ prefix */
PyMODEXPORT_FUNC PyModExport__core(void) {
static PySlot slots[] = {
PySlot_DATA(Py_mod_abi, &_core_abi_info),
PySlot_STATIC_DATA(Py_mod_name, "_core"),
PySlot_STATIC_DATA(Py_mod_methods, _core_methods),
PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
PySlot_END,
};
return slots;
}
C FILE: _CORE.F
SUBROUTINE SQUARE(B, X)
REAL B
REAL X
Cf2py intent(in) x
Cf2py intent(out) b
B = X * X
END
C END FILE _CORE.F
Package init¶
The src/example/__init__.py re-exports the pieces of the compiled _core
extension you want as your public API, so users write import example rather
than reaching into example._core:
from ._core import square
__all__ = ["square"]
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", "cython-cmake"]
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]
wheel.py-api = "cp38"
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
[tool.scikit-build]
# A free-threaded build emits the combined cp315-abi3.abi3t tag (one
# binary for every CPython 3.15+); a GIL build falls back to abi3.
wheel.py-api = "cp315.cp315t"
[build-system]
requires = ["scikit-build-core", "numpy", "f2py-cmake"]
build-backend = "scikit_build_core.build"
[project]
name = "example"
version = "0.0.1"
dependencies = ["numpy"]
Warning
Fortran is hard to compile on Windows and macOS, as it is supported by neither MSVC nor Clang; you’ll need a separate toolchain like gfortran.
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. A few things are common to
every version below: scikit-build-core requires CMake 3.15, so there’s no need
to set cmake_minimum_required lower than that. The project() line can
optionally use the SKBUILD_PROJECT_NAME and SKBUILD_PROJECT_VERSION
variables to avoid repeating information from your pyproject.toml, and should
specify exactly what language you use to keep CMake from searching for both C
and CXX compilers (the default).
cmake_minimum_required(VERSION 3.15...4.3)
project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX)
set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(_core src/${SKBUILD_PROJECT_NAME}/_core.cpp)
install(TARGETS _core LIBRARY DESTINATION ${SKBUILD_PROJECT_NAME})
If you place find Python first, pybind11 will respect it instead of the older
FindPythonInterp/FindPythonLibs mechanisms, which work, but are not as modern.
Here we set PYBIND11_FINDPYTHON 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...4.3)
project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX)
find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module)
find_package(nanobind CONFIG REQUIRED)
nanobind_add_module(_core NB_STATIC src/${SKBUILD_PROJECT_NAME}/_core.cpp)
install(TARGETS _core LIBRARY DESTINATION ${SKBUILD_PROJECT_NAME})
Nanobind places its config file such that CMake can find it from site-packages.
cmake_minimum_required(VERSION 3.15...4.3)
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(
_core
LANGUAGE python OUTPUT_DIR "${SKBUILD_PLATLIB_DIR}/${SKBUILD_PROJECT_NAME}"
SOURCES src/${SKBUILD_PROJECT_NAME}/_core.i
src/${SKBUILD_PROJECT_NAME}/_core.c)
if(WIN32)
set_property(TARGET _core PROPERTY SUFFIX ".${Python_SOABI}.pyd")
else()
set_property(TARGET _core
PROPERTY SUFFIX ".${Python_SOABI}${CMAKE_SHARED_MODULE_SUFFIX}")
endif()
target_link_libraries(_core PRIVATE Python::Module)
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
You’ll need to handle the generation of files by SWIG directly.
cmake_minimum_required(VERSION 3.15...4.3)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
find_package(Cython MODULE REQUIRED VERSION 3.0)
include(UseCython)
cython_transpile(src/${SKBUILD_PROJECT_NAME}/_core.pyx LANGUAGE C
OUTPUT_VARIABLE _core_c)
python_add_library(_core MODULE "${_core_c}" WITH_SOABI)
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
cython-cmake provides the cython_transpile helper (via include(UseCython))
that turns your .pyx file into a C source you can pass to python_add_library.
Add it to your build requirements as shown in the pyproject.toml above.
cmake_minimum_required(VERSION 3.15...4.3)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.Module
REQUIRED)
python_add_library(_core MODULE src/${SKBUILD_PROJECT_NAME}/_core.c WITH_SOABI)
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
find_package(Python ...) should always include the Development.Module
component instead of Development; see
Finding Python.
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...4.3)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.SABIModule
REQUIRED)
python_add_library(_core MODULE src/${SKBUILD_PROJECT_NAME}/_core.c WITH_SOABI
USE_SABI 3.8)
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
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.
Added in version 1.0: The free-threaded Stable ABI (abi3t) and the combined cp315.cp315t tag.
cmake_minimum_required(VERSION 3.15...4.4)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C)
find_package(
Python
COMPONENTS Interpreter Development.SABIModule
REQUIRED)
# Use the Stable ABI version and SOABI suffix scikit-build-core resolved (3.15 /
# abi3t for a free-threaded build, or abi3 for a GIL build).
if(SKBUILD_SOABI)
set(Python_SOABI ${SKBUILD_SOABI})
endif()
python_add_library(_core MODULE src/${SKBUILD_PROJECT_NAME}/_core.c WITH_SOABI
USE_SABI ${SKBUILD_SABI_VERSION})
# CMake < 4.4 has no abi3t awareness, so pass the target define ourselves.
# scikit-build-core requests abi3t via this cache variable.
if(Py_TARGET_ABI3T)
target_compile_definitions(_core PRIVATE Py_TARGET_ABI3T=0x030f0000)
endif()
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
The free-threaded Stable ABI (abi3t, PEP 803) is requested with
wheel.py-api = "cp315.cp315t". Since abi3t is a subset of abi3, a single
free-threaded build emits the combined cp315-abi3.abi3t tag and loads on
every CPython 3.15+, free-threaded or not; a GIL build falls back to
cp315-abi3.
USE_SABI is set from ${SKBUILD_SABI_VERSION} (3.15 for abi3t) rather than
hardcoded. CMake before 4.4 has no abi3t awareness, so the SOABI suffix is
taken from ${SKBUILD_SOABI} and Py_TARGET_ABI3T is defined manually; both
become unnecessary on CMake 4.4+.
Note
abi3t requires CPython 3.15+ and the PEP 793 module export mechanism, so the
classic single-phase PyInit_ entry point cannot be used.
cmake_minimum_required(VERSION 3.17...4.3)
project(${SKBUILD_PROJECT_NAME} LANGUAGES C Fortran)
find_package(
Python
COMPONENTS Interpreter Development.Module NumPy
REQUIRED)
include(UseF2Py)
f2py_add_module(_core src/${SKBUILD_PROJECT_NAME}/_core.f)
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
f2py-cmake provides the f2py_add_module helper (via include(UseF2Py))
that generates the f2py wrappers, builds the fortranobject support code, and
links it all into an importable module in a single call. Add it to your build
requirements as shown in the pyproject.toml above. You’ll need gfortran on macOS.
Finally, you install your module. The install(...) line above targets the
${SKBUILD_PROJECT_NAME} package directory rather than site-packages
directly, which is what you want for a src/example/ package layout - the
compiled _core lands next to __init__.py. A bare install (DESTINATION .)
would instead drop a single top-level extension into site-packages.
Building and installing¶
That’s it! You can install your package with any standard frontend; for example, in a virtualenv:
$ pip install .
The build guide covers all the frontends (pip, uv, pipx run build)
and the options for building, installing, and distributing your package.