Scikit-build-core is primarily a native build-backend, but you can also use it to create plugins for other build backends. We have two built-in: hatchling and setuptools. Hatchling was added by popular request, and setuptools exists mostly to allow us to replace the backend of scikit-build (classic). Both are useful, though, if you want to add a compiled extension to an existing project, or if a project is mostly Python, or if you need to combine plugins (such as building mypyc + C++).
The plugins reuse the same CMake machinery and the same [tool.scikit-build]
configuration as the standalone backend, so most of what you learned in the
scikit-build chapters carries over. The
difference is who owns the build: the host backend builds the wheel and SDist,
and scikit-build-core contributes the compiled bits as a hook.
Hatchling¶
Hatchling is the build backend behind Hatch, and it
has a nice plugin system. To add compiled extensions to a Hatchling project,
put scikit-build-core[hatchling] in your
build-system.requires, then enable the plugin by adding the
tool.hatch.build.targets.wheel.hooks.scikit-build table:
[build-system]
requires = ["scikit-build-core[hatchling]>=1"]
build-backend = "hatchling.build"
[project]
name = "hatchling_example"
version = "0.1.0"
[tool.hatch.build.targets.wheel.hooks.scikit-build]The empty hooks.scikit-build table is enough to switch the plugin on; that is
where you’d point CMake at a non-default source directory or set other options.
Configuration lives in either
tool.hatch.build.targets.wheel.hooks.scikit-build or the usual
tool.scikit-build table (or environment variables). A few backend features
don’t apply, because hatchling owns that part of the build:
wheel.cmakemust stay on — there’s no pure-Python (platlib = false) build.Building CMake during the SDist step isn’t supported.
scikit-build.generateandscikit-build.metadataare handled by hatchling’s own plugins instead.Config-settings (
-C...) aren’t available, since hatchling doesn’t pass them to plugins.
Inside CMakeLists.txt, the hatchling version is available as
${SKBUILD_HATCHLING}.
A full example¶
Here’s a mostly-Python package that adds a single C extension. The Python lives
in a normal src layout, and the compiled bits sit in their own cpp
directory:
hatchling
├── pyproject.toml
├── cpp
│ ├── CMakeLists.txt
│ └── example.c
└── src
└── hatchling_example
└── __init__.py[build-system]
requires = ["scikit-build-core[hatchling]>=1"]
build-backend = "hatchling.build"
[project]
name = "hatchling_example"
version = "0.1.0"
requires-python = ">=3.14"
[tool.hatch.build.targets.wheel.hooks.scikit-build]
cmake.source-dir = "cpp"
wheel.install-dir = "hatchling_example"
pyproject.toml
cmake.source-dir points at the CMakeLists.txt, and wheel.install-dir sends
the CMake output into the package directory so _core lands next to
__init__.py.
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 example.c WITH_SOABI)
install(TARGETS _core DESTINATION .)
cpp/CMakeLists.txt
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *square(PyObject *self, PyObject *args) {
double x;
if (!PyArg_ParseTuple(args, "d", &x)) {
return NULL;
}
return PyFloat_FromDouble(x * x);
}
static PyMethodDef methods[] = {
{"square", square, METH_VARARGS, "Square a number"},
{NULL, NULL, 0, NULL}};
static struct PyModuleDef module = {PyModuleDef_HEAD_INIT, "_core", NULL, -1,
methods};
PyMODINIT_FUNC PyInit__core(void) { return PyModule_Create(&module); }
cpp/example.c
from ._core import square
__all__ = ["square"]
src/hatchling_example/init.py
Setuptools¶
Put scikit-build-core[setuptools] in build-system.requires. You can keep
setuptools.build_meta as the backend, but using
scikit_build_core.setuptools.build_meta gets you the auto-inclusion of cmake
and ninja and config-settings, so it’s recommended if you can:
[build-system]
requires = ["scikit-build-core[setuptools]>=1"]
build-backend = "scikit_build_core.setuptools.build_meta"The plugin only activates with a cmake.source-dir setting, or with the
classic cmake_source_dir keyword in a minimal setup.py:
[tool.scikit-build]
cmake.source-dir = "."from setuptools import setup
setup(cmake_source_dir=".")Everything else configures through [tool.scikit-build] as normal.
Config-settings (-C cmake.build-type=Debug) works when building through the
PEP 517 backend, but not through setuptools.build_meta or a direct setup.py
invocation. A handful of classic setup.py keywords (cmake_args,
cmake_install_dir, cmake_install_target, …) are still honored for
compatibility.
Since setuptools owns the SDist and editable machinery, those follow
setuptools’ rules: the SDist file list comes from setuptools (only
sdist.inclusion-mode = "explicit" is supported), and editable installs
(pip install -e .) require opting in with editable.mode = "inplace", since
they write build artifacts into your source tree.
A full example¶
The same package as above, but on setuptools and configured entirely from
pyproject.toml — no setup.py:
setuptools
├── pyproject.toml
├── CMakeLists.txt
├── example.c
└── src
└── setuptools_example
└── __init__.py[build-system]
requires = ["scikit-build-core[setuptools]>=1"]
build-backend = "scikit_build_core.setuptools.build_meta"
[project]
name = "setuptools_example"
version = "0.1.0"
requires-python = ">=3.14"
[tool.setuptools.packages.find]
where = ["src"]
[tool.scikit-build]
cmake.source-dir = "."
sdist.inclusion-mode = "explicit"
sdist.include = ["**.c"]
# Required by the setuptools plugin for `pip install -e .` / `uv run`
editable.mode = "inplace"
pyproject.toml
Setuptools still discovers the Python package (here via its normal find
config), while cmake.source-dir activates the plugin and CMake installs
_core into the package. The editable.mode = "inplace" line is what lets
pip install -e . (or uv run) work, since the setuptools plugin only supports
in-place editable installs.
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 example.c WITH_SOABI)
install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME})
CMakeLists.txt
The example.c and __init__.py are identical to the hatchling example.
About plugins and build systems¶
The plugins exist because no single backend is the right answer for every project. A backend “owns” the build: it produces the SDist and wheel, and it decides which hooks get to contribute files along the way. Scikit-build-core as a plugin is one such hook — it runs CMake and drops the resulting extension modules into the wheel — while hatchling or setuptools handles the metadata, the Python sources, and the packaging.
That split is exactly what you want when:
The project is mostly Python. Keep your existing hatchling/setuptools setup and add compiled bits only where you need them.
You need to combine build steps. For example, a hatchling hook for mypyc alongside the scikit-build hook for C++.
You’re migrating. Setuptools projects (including scikit-build classic) can move onto CMake incrementally without rewriting their packaging.
If you’re starting fresh and the project is primarily a compiled extension, the
standalone scikit_build_core.build backend is still the simplest path — you
only reach for the plugins when you need to live inside another backend’s world.