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)

add_custom_command(
  OUTPUT example.c
  COMMENT
    "Making ${CMAKE_CURRENT_BINARY_DIR}/example.c from ${CMAKE_CURRENT_SOURCE_DIR}/example.pyx"
  COMMAND Python::Interpreter -m cython
          "${CMAKE_CURRENT_SOURCE_DIR}/example.pyx" --output-file example.c
  DEPENDS example.pyx
  VERBATIM)

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).

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 Development; 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: