Build a Python C Extension on Windows in 2026: MSVC, Windows SDK, pyproject.toml, and Wheels

Maintained layer, checked 2026-09-01. This guide uses the supported CPython, PyPA, setuptools, and Microsoft toolchain workflow. The complete 2019 export is preserved verbatim at the end. Its Visual C++ 2017 result is historical, and its proposed Windows 10 SDK fix was explicitly untested by the author at the time.

On Windows, a CPython C extension is normally compiled into a .pyd file. A successful build requires four things to agree: the Python interpreter, its architecture and ABI, a suitable MSVC toolchain, and Windows SDK/UCRT headers and libraries. Modern projects should declare a PEP 517 build backend in pyproject.toml, then use python -m build or python -m pip install .; invoking python setup.py as a command-line build tool is deprecated.

Know What You Are Building

The Python C API is specific to CPython. If the goal is only to call an existing C library, first consider whether ctypes or a binding generator is a better fit. A handwritten extension is appropriate when direct CPython API access, custom Python types, or very tight integration is required.

This article follows Microsoft’s supported compiler path for official Windows CPython releases. It does not cover compiling the CPython interpreter itself, cross-compiling from Linux, or mixing MinGW-built objects with an MSVC-based Python runtime.

Choose the Target Before Installing Tools

Record the exact interpreter and target architecture before building:

where.exe python
python -c "import platform, struct, sys; print(sys.executable); print(sys.version); print(platform.machine()); print(8 * struct.calcsize('P'), 'bit')"
python -m pip --version

Build and test with the same interpreter command. A 64-bit x64 Python needs an x64-targeting toolchain; Windows ARM64 and 32-bit Python are separate targets. A normal CPython extension wheel is also tied to a Python/ABI tag unless the project deliberately adopts and correctly packages the Limited API and Stable ABI.

Do not infer compatibility from the operating system alone. where.exe python may reveal a Microsoft Store alias, Conda environment, virtual environment, or another installation earlier on PATH.

Install MSVC and the Windows SDK

Use Visual Studio Installer or Microsoft C++ Build Tools. Select:

  • Desktop development with C++;
  • a currently supported MSVC C++ x64/x86 build-tools component; and
  • a current Windows SDK, which supplies the Universal CRT headers and libraries.

The precise toolset and SDK component numbers change over time, so prefer the current supported components offered by the installer instead of copying an old version number from a build log.

After installation, open Developer PowerShell for Visual Studio or the native/cross-tools command prompt that targets the interpreter’s architecture. Microsoft recommends these prepared shells because MSVC depends on coordinated PATH, INCLUDE, LIB, and LIBPATH values. Do not permanently assemble those variables by hand.

Verify One Coherent Build Shell

Inside the developer shell, change to the project directory and inspect the active tools:

where.exe python
where.exe cl
where.exe link
cl /Bv
python -c "import sys, sysconfig; print('base:', sys.base_prefix); print('include:', sysconfig.get_paths()['include']); print('EXT_SUFFIX:', sysconfig.get_config_var('EXT_SUFFIX'))"
$env:INCLUDE -split ';'

cl /Bv reports compiler details. The Python include directory should belong to the same interpreter printed by where.exe python. The INCLUDE list in a developer shell should contain MSVC and Windows SDK paths, including a versioned UCRT directory.

Setuptools can often discover Visual Studio automatically, but a developer shell makes architecture and missing-SDK problems much easier to diagnose. If using Conda, activate the intended environment in this same shell and rerun every check.

Create the Minimal Project

Use this layout in a fresh directory:

hello-extension/
├── pyproject.toml
└── src/
    └── hello.c

The distribution name will be hello-extension-example, while the importable extension module will be hello. These names may differ, but the Extension name, C initialization symbol, and Python import name must agree with one another.

Write the C Extension

Create src/hello.c:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
hello_add(PyObject *self, PyObject *args)
{
    PyObject *left;
    PyObject *right;

    (void)self;

    if (!PyArg_ParseTuple(args, "OO:add", &left, &right)) {
        return NULL;
    }

    return PyNumber_Add(left, right);
}

static PyMethodDef hello_methods[] = {
    {"add", hello_add, METH_VARARGS, PyDoc_STR("Return left + right.")},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef hello_module = {
    PyModuleDef_HEAD_INIT,
    "hello",
    "Minimal CPython extension example.",
    -1,
    hello_methods
};

PyMODINIT_FUNC
PyInit_hello(void)
{
    return PyModule_Create(&hello_module);
}

Python.h must be included before system headers that may be affected by CPython’s definitions. PyMODINIT_FUNC supplies the required export/linkage declaration, and the function name PyInit_hello matches the import name hello.

The example passes two Python objects to PyNumber_Add, so errors propagate as normal Python exceptions. It is intentionally small; production extensions also need deliberate ownership, error handling, module state, subinterpreter policy, and free-threaded-build policy.

Declare the Build in pyproject.toml

Create pyproject.toml:

[build-system]
requires = ["setuptools>=74.1"]
build-backend = "setuptools.build_meta"

[project]
name = "hello-extension-example"
version = "0.1.0"
description = "Minimal CPython C extension example"
requires-python = ">=3.9"

[tool.setuptools]
ext-modules = [
  {name = "hello", sources = ["src/hello.c"]}
]

The lower bound ensures support for declaring extension modules in setuptools’ pyproject.toml configuration. Build isolation installs the declared backend into a temporary environment; it does not install MSVC or a Windows SDK, which remain system tools.

Using a setup.py file as setuptools configuration is still supported, but executing python setup.py build, install, or bdist_wheel directly is deprecated. Prefer a build frontend.

Build, Install, and Test

From Developer PowerShell in the project root:

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip build
python -m build
Get-ChildItem .\dist\*.whl
$wheel = Get-ChildItem .\dist\*.whl | Sort-Object LastWriteTime | Select-Object -Last 1
python -m pip install --force-reinstall $wheel.FullName
python -c "import hello; print(hello.add(2, 3)); print(hello.__file__)"

The expected calculation result is 5, followed by the installed .pyd path. By default, python -m build creates an sdist and then builds a wheel from that sdist, which also checks that the C source was included in the source distribution.

For a quick local source install, this is also valid:

python -m pip install .

Use a fresh virtual environment and a clean checkout when validating release artifacts. Record the Python, pip, build-backend, compiler, and SDK versions shown in the logs.

Understand Wheel and ABI Scope

A wheel filename carries Python, ABI, and platform compatibility tags. A normal Windows extension build may produce a name ending like cp314-cp314-win_amd64.whl; the exact tags depend on the interpreter and target.

Do not rename wheel tags or copy a .pyd between Python versions to make it install. Build and test each supported target, or deliberately adopt CPython’s Limited API and configure an abi3 wheel. Limited-API source settings and wheel tagging are separate decisions and must both be correct.

Free-threaded CPython builds are another explicit target. They require extension changes and separately tagged wheels; do not claim free-threaded support merely because the ordinary build succeeds.

For a release matrix across Python versions and Windows architectures, use isolated CI runners—commonly PyPA’s cibuildwheel—and test the installed wheel rather than importing from the source tree.

Fix Unable to find vcvarsall.bat

This message is most often associated with old distutils-era discovery logic or an incomplete compiler installation. The maintained response is:

  1. Confirm the project supports the Python version being used.
  2. Update the build frontend/backend within the project’s supported ranges.
  3. Install or modify the current Microsoft C++ Build Tools with the C++ workload and Windows SDK.
  4. Open the correct Developer PowerShell or native-tools prompt.
  5. Verify where.exe cl, cl /Bv, the interpreter, and architecture again.

Do not download an arbitrary vcvarsall.bat, copy one from another Visual Studio release, or permanently paste guessed compiler paths into the global environment. If an abandoned package hard-codes an obsolete compiler, use a maintained release, patch its build configuration, or build it in a deliberately isolated legacy environment rather than weakening the current machine.

The 2019 observation that installing Visual C++ 2017 removed this error remains in the archive as a true report about that environment, not a 2026 toolchain prescription.

Fix Missing io.h or Python.h

io.h is part of the Universal CRT headers supplied through the Windows SDK. In Developer PowerShell, inspect the environment and installed header:

$env:INCLUDE -split ';'
Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\Include" -Filter io.h -Recurse | Select-Object -First 5 FullName
python -c "import sysconfig; print(sysconfig.get_paths()['include'])"

If io.h is absent, modify the Visual Studio/Build Tools installation and add a supported Windows SDK. If it exists but the compiler cannot see it, use the proper developer shell and inspect INCLUDE; do not copy the header into the Python directory.

If Python.h is missing, first confirm the exact interpreter and its sysconfig include directory. Do not combine headers from one Python installation with libraries from another. Reinstall or repair the intended Python distribution if its development files are incomplete.

Diagnose Link and Import Failures

  • LNK1112 or machine-type conflict: Python, compiled objects, and the linker target different architectures. Reopen the correct native/cross-tools shell and rebuild everything.
  • *Missing python3XY.lib or unresolved Py symbols:** the build is using the wrong Python library directory, architecture, or debug/release combination. Recheck sysconfig, interpreter origin, and stale artifacts.
  • ImportError: dynamic module does not define module export function (PyInit_...): the setuptools extension name and PyInit_name symbol do not match.
  • ImportError: DLL load failed: a dependent DLL is missing or incompatible. Locate the installed .pyd, then inspect it from a developer shell with dumpbin /dependents path\to\hello.pyd.
  • Wheel rejected as unsupported: its Python, ABI, or platform tag does not match the installer’s supported tags. Build for that target; do not rename the file.

When reporting a failure, include the full first compiler/linker error, not only the final “command failed” line. Also include the outputs of the interpreter, compiler, architecture, and build-backend checks above, after removing private paths if the report will be public.

Release Checklist

Before distributing a wheel:

  • build from a clean checkout in an isolated environment;
  • build the sdist and wheel, then install the produced wheel into a fresh environment;
  • run import and functional tests against the installed artifact;
  • test every advertised Python, ABI, and Windows architecture target;
  • ensure all C sources, headers, licenses, and generated files required by the sdist are included;
  • inspect external DLL dependencies and their redistribution terms;
  • avoid embedding local absolute paths, credentials, or build-machine data; and
  • publish from CI with least-privilege credentials, separate from untrusted build steps.

A local success proves only the current interpreter/toolchain combination. It does not prove that one wheel supports other Python versions, free-threaded builds, win32, x64, or ARM64.

Primary Documentation

Original 2019 Export (Verbatim)

The following is the complete original export, including metadata and body. It was published on 2019-04-23 and last modified two seconds later. The author reported that installing Visual C++ 2017 removed the first error, while explicitly stating that the proposed Windows 10 SDK fix had not been tried. No wording or punctuation has been corrected inside this archive.

---
id: 1900
title: 'Compile C Extension of Python on Windows'
slug: 'compile-c-extension-of-python-on-windows'
date: '2019-04-23T12:51:39'
modified: '2019-04-23T12:51:41'
status: 'publish'
link: 'https://blog.lazying.art/en/html/computer_internet/python/1900/compile-c-extension-of-python-on-windows.html'
author: 'Lachlan Chen'
categories:
  - 'Python'
---

Unable to find vcvarsall.bat

I installed Visual C++ 2017 and the error get eliminated

c:\miniconda3\include\pyconfig.h(59): fatal error C1083: Cannot open include file: ‘io.h’: No such file or directory

Installing Windows 10 SDK can solve this problem. But I have’t tried.

Leave a Reply