Tutorial: Package with C Extension#
This tutorial walks through building a Python package with a C extension for WebAssembly. We’ll start with a minimal example, build it, test it, and then cover what to do when things go wrong.
Example package#
Consider a package fastcount with this layout:
fastcount/
├── pyproject.toml
├── fastcount/
│ ├── __init__.py
│ └── _core.c
pyproject.toml:
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "fastcount"
version = "1.0.0"
[tool.setuptools]
ext-modules = [
{name = "fastcount._core", sources = ["fastcount/_core.c"]}
]
fastcount/__init__.py:
from fastcount._core import count_chars
fastcount/_core.c:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject* count_chars(PyObject* self, PyObject* args) {
const char* str;
char target;
if (!PyArg_ParseTuple(args, "sC", &str, &target))
return NULL;
long count = 0;
for (const char* p = str; *p; p++) {
if (*p == target) count++;
}
return PyLong_FromLong(count);
}
static PyMethodDef methods[] = {
{"count_chars", count_chars, METH_VARARGS, "Count occurrences of a character."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT, "_core", NULL, -1, methods
};
PyMODINIT_FUNC PyInit__core(void) {
return PyModule_Create(&module);
}
Build it#
pyodide build .
That’s it. pyodide-build:
Invokes setuptools to compile
_core.cIntercepts the
gcc/cccall and redirects it to Emscripten’semccLinks the compiled WebAssembly into a
.sofile (which is actually a Wasm binary)Packages everything into a wheel with the
pyemscripten_*_wasm32platform tag
Output:
dist/fastcount-1.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl
Test it#
pyodide venv .venv-pyodide
source .venv-pyodide/bin/activate
pip install dist/fastcount-*.whl
python -c "from fastcount import count_chars; print(count_chars('hello world', 'l'))"
# Output: 3
Using Cython#
Cython extensions work the same way — pyodide-build intercepts the C compilation step that Cython generates:
[build-system]
requires = ["setuptools", "cython"]
build-backend = "setuptools.build_meta"
No special configuration is needed for Cython. Just run pyodide build . as usual.
What’s next?#
Tutorial: Meson Package — building packages with Meson
Tutorial: CMake Package — building packages with CMake
Tutorial: Rust Package — building packages with PyO3/Maturin
How pyodide-build Works — how the compiler wrapper handles your build
Customizing Compiler Flags — fine-tuning the build
Debugging Build Failures — common error messages and systematic troubleshooting
CLI Reference — Export modes — controlling which symbols are exported (
--exports)