Skip to main content
IBM Quantum Platform

Extend Qiskit in Python with C

The Qiskit C API can be used within Python extension modules. You can write performance-critical sections of your Qiskit extensions in C to accelerate them, and then safely distribute these to your users.

This guide walks you through the process of defining a complete extension module, configuring its build process, and exposing it to Python users. The package provides a simple port of AddSpectatorMeasures from the Qiskit addons to C. This is a real custom pass with a real use case in the Qiskit addons.

Tip

You might find the following external resources helpful:

The Qiskit C API is exposed for Python extension modules in a very similar manner to the NumPy C API. If you have previously programmed a NumPy extension, you will find the Qiskit process familiar.

Warning

The Qiskit C API is still experimental. Thus, there is not yet a fully stable programming or binary interface, and there might be breaking changes between minor versions.

For example, an extension module using Qiskit v2.4.0 at build time is guaranteed to work with Qiskit v2.4.1 at runtime, but might break when using Qiskit v2.5.0 at runtime.


Requirements

Start from a clean directory.

You must have the standard C compiler toolchain available for your platform. You must also have a version of Python that includes its C API headers (this is standard).

You should be familiar with, or prepared to look up, the individual functions and objects available in the Qiskit C API. You should have some familiarity with C programming.


Create the directory structure

We will use a src-based directory structure and a simple setuptools-based build system. These instructions should be easy adapt to any build system that can build extension modules.

The final structure will look like:

extension-module
├── pyproject.toml
├── setup.py
└── src
    └── spectator_measures
        ├── __init__.py
        └── _coremodule.c

In summary:

  • pyproject.toml defines the standard static metadata about the Python package we are creating, including its name, author, and build- and run-time dependencies.
  • setup.py contains the minimal dynamic configuration we need to build our extension module.
  • src/spectator_measures/__init__.py defines the user-facing interface and provides some code to interface with Qiskit's Python-space components.
  • src/spectator_measures/_coremodule.c defines the C extension module, which will contain all the performance-critical code of our package.

We will examine each file in detail, building up the package with its extension module.


Define the package metadata

Begin by defining the pyproject.toml file. This is standard for a setuptools-based project, although qiskit is an additional requirement in the build-system.requires array, in addition to setuptools.

pyproject.toml
[build-system]
requires = [
    "setuptools",
    "qiskit~=2.4.0",
]
build-backend = "setuptools.build_meta"

[project]
name = "spectator_measures"
authors = [
    { name = "Qiskit Developer" },
]
version = "0.0.1"
dependencies = [
    "qiskit~=2.4.0",
]
# If you intend to release your package, you should
# also set the `license` information, and so on.

[tool.setuptools]
package-dir = {"" = "src"}

As of Qiskit v2.4, the C API is not yet stable outside of minor versions (for example, the C API for v2.4.0 will be compatible with v2.4.1 but not v2.5.0). In the future, we intend to extend this stability to with major versions. For now, set Qiskit's runtime version in project.dependencies to match the minor version used at build time.

In many pure-Python setuptools-based projects, it would be sufficient to have the pyproject.toml file. However, our module needs access to the Qiskit C API header files during its build process. Starting with v2.4, these are included in the Qiskit SDK Python distributions. To locate the directory containing them, run qiskit.capi.get_include(). This results in a setup.py file that looks like:

setup.py
import qiskit
from setuptools import setup, Extension

core_ext = Extension(
    # The fully qualified module name of the extension.
    name="spectator_measures._core",
    # The C source files needed for the extension.  The file
    # name is conventionally `<mod>module.c`, where `<mod>`
    # is the module name (`_core`, in this case).
    sources=["src/spectator_measures/_coremodule.c"],
    # Directories containing additional header files used in
    # the build process.
    include_dirs=[qiskit.capi.get_include()],
)
setup(ext_modules=[core_ext])

Most of the package information is defined in pyproject.toml, and setuptools.setup() will also read that file.

Tip

See the setuptools User Guide for more information on configuring setuptools-based projects.


Write the Python-space wrapper

It is technically possible to define everything in a Python extension from C. In practice, it is easier to interact with other Python-space code from Python itself.

This package defines a custom transpiler pass that derives from the Python-space qiskit.transpiler.TransformationPass class, but uses a function from the C extension module for all of its business logic. This looks like:

src/spectator_measures/__init__.py
from qiskit.transpiler import TransformationPass, Target
from . import _core

__version__ = "0.0.1"
__all__ = ["AddSpectatorMeasures"]


class AddSpectatorMeasures(TransformationPass):
    def __init__(
        self,
        target: Target,
        *,
        include_unmeasured: bool = False,
        creg_name: str | None = None,
        add_barrier: bool = True
    ):
        super().__init__()
        self.target = target
        self.include_unmeasured = include_unmeasured
        self.creg_name = creg_name
        self.add_barrier = add_barrier

    def run(self, dag):
        # Delegate to our C extension module.
        _core.add_spectator_measures(
            dag,
            self.target,
            include_unmeasured=self.include_unmeasured,
            creg_name=self.creg_name,
            add_barrier=self.add_barrier,
        )
        return dag

The exact details of this pass are unimportant for this guide. If you are interested, you can consult the AddSpectatorMeasures API documentation in qiskit-addon-utils. This guide produces a simple port of that pass, without support for control-flow operations.


Write the C extension module

This section is concerned with the actual C extension. This is the most complex file in the project, so we will split it into stages.

Configure the header files

When building a Python extension module, you must include Python.h before any other file. To use the Qiskit C API in an extension module, you must define the macro QISKIT_PYTHON_EXTENSION before including qiskit.h.

Our includes then look like:

src/spectator_measures/_coremodule.c
#define QISKIT_PYTHON_EXTENSION
#include <Python.h>
#include <qiskit.h>

#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

Write the pure C API code

Next, write all of the business logic as pure Qiskit C API code. We will expose this logic to Python space in the following section.

This section contains only pure Qiskit C API code. It uses the C API types:

  • QkDag *, corresponding to the Python-space DAGCircuit.
  • QkTarget *, corresponding to the Python-space Target.
  • QkNeighbors, a native C API type representing two-qubit coupling constraints.
  • QkCircuitInstruction, a native C API type for querying individual instructions.

The first two form part of our interaction with Python space, but when working with them, we only need to consider the pure C API. There is no interaction with the Python interpreter in this code.

Note that all functions and symbols defined in this section are declared with static linkage. This is because the Python interpreter will not link against this extension module; we will provide the interpreter with details of the available functions in the next section.

We will not dwell on the algorithmic details of this code; it is instructive to use a meaningful transpiler pass for the demonstration, but the precise implementation of the algorithm is not important to this guide.

src/spectator_measures/_coremodule.c (appended)
/**
 * The default name to use for `creg_name` if none is supplied.
 */
static char DEFAULT_CREG_NAME[] = "spec";

/**
 * Is there a 2q link from the given qubit to any active qubit?
 */
static bool adjacent_to_active(QkNeighbors *adj, uint32_t qubit,
                               bool *active) {
    for (uint32_t offset = adj->partition[qubit];
         offset < adj->partition[qubit + 1]; offset++) {
        if (active[adj->neighbors[offset]]) {
            return true;
        }
    }
    return false;
}

/**
 * A transpiler pass that adds terminal measurements to all "spectator"
 * qubits.
 */
static uint32_t add_spectator_measures(QkDag *dag,
                                       const QkTarget *target,
                                       bool include_unmeasured,
                                       const char *creg_name,
                                       bool add_barrier) {
    uint32_t num_spectators = 0;
    uint32_t num_qubits = qk_dag_num_qubits(dag);
    uint32_t num_instructions = qk_dag_num_op_nodes(dag);
    bool *active = calloc(num_qubits, sizeof(*active));
    bool *is_additional_spectator =
        calloc(num_qubits, sizeof(*is_additional_spectator));
    uint32_t *spectators = malloc(num_qubits * sizeof(*spectators));
    uint32_t *topological =
        malloc(num_instructions * sizeof(*topological));
    QkNeighbors neighbors;
    QkCircuitInstruction instruction;

    qk_neighbors_from_target(target, &neighbors);
    qk_dag_topological_op_nodes(dag, topological);

    for (uint32_t i = 0; i < num_instructions; i++) {
        qk_dag_get_instruction(dag, topological[i], &instruction);
        if (!strcmp(instruction.name, "barrier")) {
            // Barriers don't count for the purposes of determining
            // final measurements, either.
            qk_circuit_instruction_clear(&instruction);
            continue;
        }
        // If we're not adding measurements to "unmeasured" active
        // qubits, then nothing counts as an additional "maybe
        // spectator".  If we are, then it's a maybe spectator if its
        // last visited instruction was not a measure.
        bool additional_spectator =
            include_unmeasured && strcmp(instruction.name, "measure");
        for (uint32_t *qarg = instruction.qubits;
             qarg != instruction.qubits + instruction.num_qubits;
             qarg++) {
            active[*qarg] = true;
            is_additional_spectator[*qarg] = additional_spectator;
        }
        qk_circuit_instruction_clear(&instruction);
    }

    for (uint32_t qubit = 0; qubit < num_qubits; qubit++) {
        bool is_spectator =
            !active[qubit] &&
            adjacent_to_active(&neighbors, qubit, active);
        is_spectator = is_spectator || is_additional_spectator[qubit];
        if (is_spectator) {
            spectators[num_spectators] = qubit;
            num_spectators += 1;
        }
    }

    if (num_spectators) {
        uint32_t clbit = qk_dag_num_clbits(dag);
        creg_name = creg_name ? creg_name : DEFAULT_CREG_NAME;
        QkClassicalRegister *creg =
            qk_classical_register_new(num_spectators, creg_name);
        qk_dag_add_classical_register(dag, creg);
        qk_classical_register_free(creg);
        if (add_barrier) {
            qk_dag_apply_barrier(dag, NULL, num_qubits, false);
        }
        for (uint32_t i = 0; i < num_spectators; i++) {
            qk_dag_apply_measure(dag, spectators[i], clbit + i, false);
        }
    }

    qk_neighbors_clear(&neighbors);
    free(topological);
    free(spectators);
    free(is_additional_spectator);
    free(active);
    return num_spectators;
}

Write the Python interaction code

All of the business logic is now defined in pure C. Next, it needs to be safely exposed to Python.

To begin, define the only function that will be exposed to Python. This must follow a defined signature, which is purely in terms of Python types that look like a fn(self, *args, **kwargs) method. We have to return a PyObject *, which is the generic form of any Python object.

The complete function looks like:

src/spectator_measures/_coremodule.c (appended)
static PyObject *py_add_spectator_measures(PyObject *self,
                                           PyObject *args,
                                           PyObject *kwargs) {
    // Define space to hold the C-native handles we will parse out of the
    // Python-space inputs.
    QkDag *dag;
    QkTarget *target;
    const char *creg_name;
    int include_unmeasured, add_barrier;

    // This `kwlist` and `PyArg_Parse*` setup is standard Python C API
    // programming for extension modules.  We will examine the use of
    // Qiskit C API functions within it afterwards.
    static char *const kwlist[] = {
        "dag",       "target",      "include_unmeasured",
        "creg_name", "add_barrier", NULL};
    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&O&|pzp", kwlist,
                                     qk_dag_convert_from_python, &dag,
                                     qk_target_convert_from_python,
                                     &target, &include_unmeasured,
                                     &creg_name, &add_barrier)) {
        // An error has occurred. The Python exception state will already
        // be set, so we need to return the error indicator.
        return NULL;
    }

    // Now we have C-native types, we can delegate to our C logic.
    add_spectator_measures(dag, target, include_unmeasured, creg_name,
                           add_barrier);
    Py_RETURN_NONE;
}

In brief, the function:

  1. Follows a defined signature to accept arbitrary Python arguments.
  2. Defines space to store C-native objects parsed out of the Python arguments.
  3. Calls a parsing function to extract the C-native objects, configured with the list of expected arguments, keyword arguments, and the functions to use to convert them. If this fails, the function propagates the error.
  4. Delegates to the C-native business logic of the previous section, which mutates the DAG in place.
  5. Returns the Python-space None object.

The most complex logic is all inside PyArg_ParseTupleAndKeywords. This is well documented in the CPython documentation on parsing arguments, which you should consult for further information.

The Qiskit C API provides several functions with names like qk_*_convert_from_python, which are designed as "converter" functions for use with PyArg_Parse*functions. These correspond to the O& keys in the format string; here, we used qk_dag_convert_from_python and qk_target_convert_from_python. These functions borrow the C-native object from the Python argument they are derived from. This means that mutations will propagate to Python space, but also that you should take care not to release your reference to the Python object that backs them, while using the result. This is standard for Python C API programming.

Next, we define the information about this module and the function it contains, so we can pass it to Python space:

src/spectator_measures/_coremodule.c (appended)
static PyMethodDef core_methods[] = {
    // This entry is our function, cast to the correct type.
    {"add_spectator_measures",
     (PyCFunction)(void (*)(void))py_add_spectator_measures,
     METH_VARARGS | METH_KEYWORDS, ""},
    // A sentinel marking the end of the list.
    {NULL, NULL, 0, NULL},
};
static struct PyModuleDef core_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "_core",
    .m_methods = core_methods,
};

This method table and module-definition structure are described in more detail in the CPython documentation on module initialization.

Finally, tell Python how to initialize the module. This is the only function in the C file that is exported. Its name must exactly match the pattern PyInit_<mod>, where <mod> is the (unqualified) module name. In this case, the fully qualified module name is spectator_measures._core, and the unqualified name is _core, so our function must be called PyInit__core, with the double underscore.

src/spectator_measures/_coremodule.c (appended)
PyMODINIT_FUNC PyInit__core(void) {
    // This line is critical to use the Qiskit C API. Your code will
    // likely be immediately terminated by the operating system if you
    // forget to do this.
    if (qk_import() < 0) {
        return NULL;
    };
    // The standard Python call to initialize a module.
    return PyModuleDef_Init(&core_module);
}

The PyMODINIT_FUNC and PyModuleDef_Init symbols are both standard Python C API programming. The Qiskit-specific component is qk_import(). It is critical that you call this function during the initialization function of your module; you will not be able to call any Qiskit C API functions until this has been successfully executed.


Use the package from Python

This is now a complete package, including a C extension module. Because only standard tooling was used, and no non-standard system libraries are linked during build time, the build process is simple.

You can use any PEP-517-compatible build tool. As a minimal example, you can run the following command in the repository root to install the package.

pip install .

This compiles the C extension module and installs the complete Python package in your environment.

An example use of this custom transpiler pass is:

from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap, Target
from spectator_measures import AddSpectatorMeasures

num_qubits = 10
qc = QuantumCircuit(num_qubits)
qc.x(0)
qc.x(5)

target = Target.from_configuration(
    basis_gates=["x", "sx", "rz", "cx"],
    num_qubits=num_qubits,
    coupling_map=CouplingMap.from_line(num_qubits),
)
pass_ = AddSpectatorMeasures(target)
pass_(qc).draw()

The result of this is:

        ┌───┐ ░
   q_0: ┤ X ├─░──────────
        └───┘ ░ ┌─┐
   q_1: ──────░─┤M├──────
              ░ └╥┘
   q_2: ──────░──╫───────
              ░  ║
   q_3: ──────░──╫───────
              ░  ║ ┌─┐
   q_4: ──────░──╫─┤M├───
        ┌───┐ ░  ║ └╥┘
   q_5: ┤ X ├─░──╫──╫────
        └───┘ ░  ║  ║ ┌─┐
   q_6: ──────░──╫──╫─┤M├
              ░  ║  ║ └╥┘
   q_7: ──────░──╫──╫──╫─
              ░  ║  ║  ║
   q_8: ──────░──╫──╫──╫─
              ░  ║  ║  ║
   q_9: ──────░──╫──╫──╫─
              ░  ║  ║  ║
spec: 3/═════════╩══╩══╩═
                 0  1  2
Was this page helpful?
Report a bug, typo, or request content on GitHub.