scikit-build-core and replacing setuptools with CMake as a Python build backend
A next generation Python CMake adaptor and Python API for plugins
At a glance
- What is it?
- scikit-build-core is a PEP 517 build backend that drives CMake to produce Python wheels and sdists, and it is a ground-up rewrite of the older scikit-build rather than a wrapper around it. The interesting engineering is the compatibility machinery, since it can pin its own defaults to whatever a package declared, and the interesting constraint is that CMake remains a hard dependency no matter how the configuration looks.
- Who is it for?
- scikit-build-core is a good fit for a project with a real CMake build that has outgrown setuptools, because it keeps the thousands of lines of distutils configuration out of the package and puts the build logic in CMakeLists.txt files instead. It is a poor fit for a pure Python package, since it adds CMake and Ninja to your build environment for no gain, and a poor fit if your platform has no CMake at all.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the backend replaces, and what it does not
scikit-build-core is a build backend for Python that uses CMake to build extension modules, with a static configuration system in pyproject.toml and flexibility through CMake itself. The description says it was initially developed to support the demanding needs of scientific users, but can build any sort of package that uses CMake.
It is a ground-up rewrite of the classic scikit-build, which was setuptools based. The README lists the classic features that are also present here: support for or by most operating systems, compilers, IDEs and libraries, support for C++ features and other languages such as Fortran, support for multithreaded builds, simple CMakeLists.txt files instead of up to thousands of lines of fragile setuptools or distutils code, and cross-compile support for Apple Silicon and Windows ARM.
That last bullet is the clearest statement of what a CMake-based backend buys. The classic approach was to encode the build in Python, which meant every platform and compiler quirk had to be reimplemented in Python. Moving that into CMake means the build logic is the same logic the C++ ecosystem already uses.
The improvements over classic are listed separately and are mostly about developer experience rather than capability. Better warnings, errors and logging, and no warning about unused variables. Automatically adds Ninja and/or CMake only as required, and no dependency on setuptools, distutils or wheel. A powerful config system including config options support. Automatic inclusion of site-packages in CMAKE_PREFIX_PATH. FindPython is backported if running on CMake below 3.26.1, and that backport is configurable.
The primary language is Python, the licence is Apache-2.0, the homepage is scikit-build-core.readthedocs.io, the last push was on 2026-10-07, and the recent releases are v1.1.1 from 2026-10-02, v1.1.0 from 2026-09-25 and v1.0.3 from 2026-07-12. The project is not archived and holds a public community meeting every month.
The minimum viable pyproject.toml and CMakeLists.txt
The example section shows the smallest configuration that works, and it is worth quoting in full because the minimal version is genuinely minimal.
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
[project]
name = "scikit_build_simplest"
version = "0.0.1"There are only three things to get right in the manifest. You require scikit-build-core in build-system.requires, you set the build backend to scikit_build_core.build, and you have a project name and version. The README points out that you do not need to specify cmake or ninja, because scikit-build-core will require them automatically if the system versions are not sufficient.
That last sentence is a genuine design choice rather than a convenience. The backend treats the build tools as dependencies it can pull in, so a developer on a machine with an old CMake still gets a working build, and a machine with a good CMake does not have its version overridden.
The CMake side is a seven-line file:
cmake_minimum_required(VERSION 3.15...4.4)
project(scikit_build_simplest LANGUAGES C)
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
Python_add_library(_module MODULE src/module.c WITH_SOABI)
install(TARGETS _module DESTINATION scikit_build_simplest)Two details carry the weight. The version range in cmake_minimum_required is 3.15...4.4, so the file declares a floor and a tested ceiling in one expression. And Python_add_library is called with WITH_SOABI, which produces a module filename that carries the ABI tag, which matters on platforms where the extension filename has to match what the interpreter expects.
The install destination is relative to site-packages, so the extension lands at scikit_build_simplest/_module.* once the package is pip installed. The README adds that you need to install everything you want into the full final path inside site-packages, so you will usually prefix everything by the package name, including any __init__.py, which CMake can place alongside the extension.
The fastest way to a starting point is the CLI. scikit-build init, available via uvx scikit-build-core init as well, generates a minimal CMake plus scikit-build-core starter project.
minimum-version, and how the backend keeps your build stable
The most important optional configuration in the README is four lines of TOML, and it is the piece that distinguishes this backend from most.
[build-system]
requires = ["scikit-build-core>=1.0"]
build-backend = "scikit_build_core.build"
[tool.scikit-build]
minimum-version = "build-system.requires"Setting minimum-version to build-system.requires tells scikit-build-core to read your own minimum requirement and stay in compatibility mode for that version. The stated effect is that if changes are made to defaults in the future, your package will continue to build exactly the same.
That is a thoughtful answer to a real problem. The backend has defaults, and defaults change. A package that says it needs scikit-build-core 1.0 would otherwise pick up whatever the newest release does by default and might produce a different wheel from the same source. Pinning the compatibility mode means the output is a function of your declared floor and your source, not of what happened to be installed.
It also gives a package an escape hatch. If a new default is wrong for your project, you are not forced to adopt it; you declare the version you meant and get that version's behaviour.
The configuration system as a whole is broader than that one key. All configuration options can be placed in pyproject.toml, passed via -C in pip, uv and build, or set as environment variables. The prefixes follow the source: tool.scikit-build in TOML, skbuild. for -C options, and SKBUILD_* for environment variables.
Having the same option reachable three ways matters for a build backend, because a build is often driven by whatever the CI system happens to provide. The environment variable form lets a CI job override a setting without patching the manifest, and the -C form lets pip and uv pass the same value.
The minimum supported CMake is 3.15 and the minimum supported Python is 3.9, with 3.8 and above for the 1.0.x releases and older.
The feature list that says more about the ecosystem than the backend
Several entries in the feature list are worth pulling out, because they tell you what problems the project considers important rather than merely listing capabilities.
Reproducibility comes first. SDists are reproducible by default, with the note that UNIX and Python 3.9 or later are the relevant case and that uncompressed comparison is recommended, and wheels can be made reproducible too as an opt-in.
Caching between builds is supported as an opt-in by setting build-dir. That option name tells you what it does: it keeps the CMake build directory around instead of reconfiguring from scratch on every build, which is the difference between a fast incremental build and a slow one.
The dynamic metadata plugins are a distinctive feature. There are several integrated dynamic metadata plugins, supporting the cross-backend [[tool.dynamic-metadata]] standard. That means metadata can be computed at build time and injected into the wheel, and the standard is cross-backend so the same TOML works with other backends that implement it.
Editable mode support comes with optional experimental auto rebuilds on import and optional in-place mode. The experimental label on the auto-rebuild is the important word: rebuilding a C++ extension when the user imports it is convenient and can be surprising.
There is support for writing out to extra wheel folders for scripts, headers and data, and for selecting install components and build targets, plus dedicated entrypoints for module and prefix directories. Those are the mechanisms that decide where files land inside the wheel, which is where a lot of packaging pain comes from.
Two platform entries stand out. WebAssembly is supported through Emscripten and Pyodide, and free-threaded Python 3.13 and later is supported, including the free-threaded stable ABI described by PEP 803. The classifiers in the project metadata include a Programming Language :: Python :: Free Threading :: 4 - Resilient entry, so the free-threaded build is a first-class target rather than an experiment.
There is also support for the Limited API and Stable ABI and pythonless tags through a config option, and FindPython support for PyPy SOABI. The Stable ABI matters because it is what lets a wheel built on one Python work on others.
Where the setuptools and hatchling integrations fit, and the warning attached
The recommended interface is the native pyproject builder, but two other backends are available, and the README attaches a warning to them that deserves attention.
Setuptools integration uses the [setuptools] extra, and the Hatchling plugin uses the [hatchling] extra. These are for packages that are not ready to make scikit-build-core their own build backend but still want the CMake-driven build.
The warning says the setuptools and hatchling backends might move to standalone packages in the future, so depend on them via the scikit-build-core[setuptools] and scikit-build-core[hatchling] extras to stay protected if they do.
That is a small piece of dependency advice with a large consequence. If you write dependencies = ["scikit-build-core[setuptools]"] rather than depending on the extra by name directly, then when the backend is split out into its own package, your requirement still resolves to something that exists. If you depend on the extra's contents by name, or if the extra disappears, the install breaks.
The other notable detail in the packaging metadata is the comment about build dependencies. It says cmake and possibly ninja are also required if those are not already present, and that this is user controllable, but a system version is fine. So the backend does not force its own pinned CMake; it will use a sufficiently new system CMake if there is one and fall back to fetching its own otherwise.
The package's own dependencies are modest: exceptiongroup for Python below 3.11, packaging, pathspec, tomli and typing-extensions for Python below 3.11. The metadata declares requires-python of 3.9 or later, and the classifiers cover Python 3.9 through 3.15.
One more piece of the metadata is worth noting for anyone reading the project as a model: it builds itself with hatchling and hatch-vcs, with the version dynamic, which is the same pattern scikit-build-core recommends to its own users. The project eats its own cooking, which is a reasonable signal that the recommended configuration works.
Where scikit-build-core is the wrong choice
Three limits follow from the design.
The first is that it is the wrong tool for a pure Python package. Every one of the advantages assumes there is something to compile. If your package is Python source with no extension module, adding a CMake dependency to your build environment buys nothing and costs a toolchain, a configure step and a second failure mode on machines where CMake is missing. The README's own framing supports this: it was developed for the demanding needs of scientific users and can build any sort of package that uses CMake, which is a package that uses CMake.
The second is the dependency on CMake being present and current enough. The minimum supported CMake is 3.15, and FindPython is backported from CMake 3.26.1 for older versions, which is a sign of how far back the support goes and also of the amount of compatibility code involved. On a platform where CMake cannot run at all, this backend does not apply, and the README says nothing about such a case.
The third is that the configuration surface is wide, and wide surfaces have sharp edges. The same options exist in three places with three prefixes, the defaults are versioned, and there is a compatibility mode to pin them. That is a good design, but it means the set of things that can change your build output is larger than in a plain setuptools package, and a project that wants predictable behaviour has to set minimum-version deliberately rather than rely on whatever the newest release defaults to.
The alternative worth comparing is staying with setuptools and calling CMake from a custom build command. The difference in approach is where the build logic lives. Calling CMake from setuptools leaves you with a Python build that shells out to CMake, which means you keep the setuptools configuration surface and add CMake on top of it. scikit-build-core inverts that: CMake is the build system and the Python packaging standard is the wrapper around it. For a package with a simple C extension and a small build, the setuptools approach is less machinery. For a package with a real CMake project, or one that also builds Fortran, or that cross-compiles for Apple Silicon and Windows ARM, the inversion is the point.
There is also the middle option the project itself offers, the setuptools and hatchling integrations, which give you the CMake-driven build without giving up your existing backend.
Editorial conclusion
scikit-build-core is a good fit for a project with a real CMake build that has outgrown setuptools, because it keeps the thousands of lines of distutils configuration out of the package and puts the build logic in CMakeLists.txt files instead. It is a poor fit for a pure Python package, since it adds CMake and Ninja to your build environment for no gain, and a poor fit if your platform has no CMake at all. Before adopting, set minimum-version to build-system.requires so your build keeps behaving the same way across scikit-build-core releases, and read the note about the setuptools and hatchling extras, because the documentation warns those backends may move to standalone packages.
Frequently asked questions
How do I add scikit-build-core to my pyproject.toml?
Require scikit-build-core in build-system.requires and set the build backend to scikit_build_core.build. The README's minimal example needs only that, plus a project name and version, and you do not need to specify cmake or ninja because scikit-build-core requires them automatically if the system versions are not sufficient.
What does the minimum-version setting do in scikit-build-core?
Setting minimum-version to build-system.requires makes scikit-build-core read your own minimum requirement and stay in compatibility mode for that version, so if defaults change in a future release your package continues to build exactly the same.
What are the minimum supported versions for scikit-build-core?
The minimum supported CMake is 3.15 and the minimum supported Python is 3.9, with 3.8 and above supported for the 1.0.x releases and older. FindPython is backported from CMake 3.26.1 for older CMake versions, and that backport is configurable.
How do I configure scikit-build-core options?
All configuration options can be placed in pyproject.toml, passed via -C in pip, uv and build, or set as environment variables. The prefixes are tool.scikit-build for TOML, skbuild. for -C options, and SKBUILD_* for environment variables.
How do I start a new project with scikit-build-core?
Run scikit-build init, which is also available as uvx scikit-build-core init. It generates a minimal CMake plus scikit-build-core starter project, and there are more examples in the tests/packages directory of the repository.
Which platforms does scikit-build-core support?
Beyond the usual operating systems and compilers, it documents cross-compile support for Apple Silicon and Windows ARM, support for WebAssembly through Emscripten and Pyodide, and support for free-threaded Python 3.13 and later including the free-threaded stable ABI described by PEP 803.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/scikit-build-scikit-build-core)Was this useful?