Add Python ABI blog post#979
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
rgommers
left a comment
There was a problem hiding this comment.
Looks like a good start - I had some time, so did a read through. My comments focus only on the main conceptual things that stood out to me.
|
I did another pass today to address Ralf's comments and try to fill in the rest of the text. It's not quite done yet but hopefully I can finish this off next week. If anyone would like to read this over and give me comments, please feel free. |
JelleZijlstra
left a comment
There was a problem hiding this comment.
Found a number of typos and other small issues
Thanks so much for the copy-editing! Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
rgommers
left a comment
There was a problem hiding this comment.
This is looking very close now. I quite like the new figures! A few more comments.
| What isn't the C API? | ||
| The C API is purely a construct of the C programming language. | ||
| Code written in languages that aren't C can call into a C API, but only by using the platform-specific and architecture-specific calling conventions used by all code that executes directly on the CPU, abstracting away functionality that is not expressible in C. | ||
| This is when it becomes important to think about the ABI. |
There was a problem hiding this comment.
This seems to imply (to me at least) that the ABI matters for C++, Rust et al., but not for C. That's not true of course, so you may want to tweak this just a little to avoid giving that impression.
There was a problem hiding this comment.
I added a paragraph explaining how a C ABI in particular is very important for both C++ and Rust to enable binary interoperability.
|
|
||
| A platform ABI determines things like the exact way machine code needs to pass arguments to a function. For example, on the [`x86_64`](https://wiki.osdev.org/System_V_ABI#x86-64) architecture, only the first six non-floating-point arguments of a function are passed via registers, the remaining arguments are passed via the stack. | ||
| Normally, developers do not need to worry about details like this: compilers automatically generate machine code appropriate for whatever platform ABI a developer wants to target. | ||
| However, it _is_ important to know that different operating systems and CPU architectures have unique ABIs and that code compiled for one platform ABI is completely unusable on another platform ABI. |
There was a problem hiding this comment.
This paragraph is solid, so I'm not sure if it needs a change. However, it did occur to me here that both libc flavor and compiler toolchain choice go into the ABI, with practical consequences (e.g. manylinux vs. musllinux, or mingw-w64 incompatibility with MSVC). The phrase "libc" doesn't appear in the post at all currently.
There was a problem hiding this comment.
I added a paragraph explaining that the above is a slight oversimplification and links to a nice blog post I found that goes into the complexities of target triples.
There was a problem hiding this comment.
I've been using the term “the (underlying) platform” for this. Maybe you can too?
| However, it _is_ important to know that different operating systems and CPU architectures have unique ABIs and that code compiled for one platform ABI is completely unusable on another platform ABI. | |
| However, it _is_ important to know that the underlying platform -- details like | |
| different operating systems and CPU architectures -- have unique ABIs and that code compiled for one platform ABI is completely unusable on another platform ABI. |
| All Python objects correspond in C to an instance of `PyObject` or a struct that extends the `PyObject` struct. | ||
| Until free-threaded Python (more on that below), the `PyObject` struct had the following layout on 64-bit architectures: | ||
|
|
||
| ```C |
There was a problem hiding this comment.
syntax highlighting is missing, here and further down - perhaps lowercase c?
| Currently, `abi3t` support in build tools and bindings generators is mixed at time of writing in June 2026, as Python 3.15 is still in a pre-release stage. | ||
| As the Python 3.15 final release approaches, expect to see build backend support improved. | ||
| [PyO3](https://pyo3.rs/v0.29.0/features.html#abi3t) and [Maturin](https://www.maturin.rs/bindings.html?highlight=abi3t#py_limited_apiabi3) fully support Python 3.15, so most Rust extensions should be ready for testing. | ||
| [Scikit-build-core](https://scikit-build-core.readthedocs.io/en/latest/configuration/index.html#customizing-the-output-wheel) and [CMake](https://cmake.org/cmake/help/latest/module/FindPython3.html) also support abi3t. |
There was a problem hiding this comment.
CMake isn't a build backend, I wouldn't mention it here. scikit-build-core relies on CMake having support in a pretty much identical way to meson-python relying on Meson.
| [Scikit-build-core](https://scikit-build-core.readthedocs.io/en/latest/configuration/index.html#customizing-the-output-wheel) and [CMake](https://cmake.org/cmake/help/latest/module/FindPython3.html) also support abi3t. | ||
| Cython supports abi3t via [an experimental branch](https://github.com/cython/cython/issues/7399). | ||
| Setuptools support will be added once [PR #5193](https://github.com/pypa/setuptools/pull/5193) is merged and appears in a release. | ||
| Meson-python support also lives in [an open PR](https://github.com/mesonbuild/meson-python/pull/856) currently. |
There was a problem hiding this comment.
In case we release that before this post goes up (PR is about ready right now), let's update this:)
| alt: 'Data APIs logo next to logos of NumPy, CuPy, PyTorch and JAX' | ||
| hero: | ||
| imageSrc: posts/array-api-aot-jit/array-api-aot-jit.png | ||
| imageSrc: /posts/array-api-aot-jit/array-api-aot-jit.png |
There was a problem hiding this comment.
I was having trouble building the blog locally because of this.
| <div className="overflow-x-auto"> | ||
| <table className="mx-auto w-auto min-w-[42rem]"> | ||
| <thead> | ||
| <tr> | ||
| <th>CPython version</th> | ||
| <th>Non-free-threaded</th> | ||
| <th>Free-threaded</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| <tr> | ||
| <td>3.12</td> | ||
| <td rowSpan={3} className="align-middle text-center"><code>abi3</code></td> | ||
| <td>—</td> | ||
| </tr> | ||
| <tr> | ||
| <td>3.13</td> | ||
| <td>—</td> | ||
| </tr> | ||
| <tr> | ||
| <td>3.14</td> | ||
| <td><code>cp314t</code></td> | ||
| </tr> | ||
| <tr> | ||
| <td>3.15</td> | ||
| <td colSpan={2} rowSpan={3} className="align-middle text-center"><code>abi3.abi3t</code></td> | ||
| </tr> | ||
| <tr> | ||
| <td>3.16</td> | ||
| </tr> | ||
| <tr> | ||
| <td>3.17 and later</td> | ||
| </tr> | ||
| </tbody> | ||
| </table> | ||
| </div> |
There was a problem hiding this comment.
Without visible grid, it's hard to tell which labels refer to which rows.
There was a problem hiding this comment.
Do you have any idea how to fix it? This table was cargo-culted from another post.
There was a problem hiding this comment.
Claude ended up coming up with something eventually that looks much nicer than what was there before. Thanks for the prompt.
encukou
left a comment
There was a problem hiding this comment.
A great blog post!
I do have a few comments, but nothing major:
| In order to use it, a C or C++ program must define the `Py_BUILD_CORE` compiler macro, which is equivalent to declaring that your program is a part of the CPython interpreter itself. | ||
| As [the CPython developer guide](https://devguide.python.org/developer-workflow/c-api/#the-internal-api) indicates, the internal API is subject to change at any moment and should not be relied on. | ||
| Defining `Py_BUILD_CORE` and opting into using the internal C API is equivalent to saying you're willing to take on maintenance for code that can and will break at any time. | ||
| For 99.9% of people who are not CPython contributors, the internal API should not be used. |
There was a problem hiding this comment.
Maybe it's worth mentioning that this API ties you to a specific build (rather than just version) of CPython -- so it has some use for people who build a modified interpreter from source.
| This fact determines much of the design of the [binary wheel distribution format](https://packaging.python.org/en/latest/specifications/binary-distribution-format/), which we'll discuss in more detail below. | ||
|
|
||
| The biggest consequence is that each platform and CPU architecture, each with its own distinct platform ABI, requires its own unique builds. | ||
| This is one reason projects like `NumPy` distribute so many binary wheels with each release: projects need to build binaries for each platform ABI they want to support. |
There was a problem hiding this comment.
Maybe link "so many binary wheels" to https://pypi.org/project/numpy/2.5.0/#files ?
| This optimization is useful sometimes in the interpreter, so it's defined. | ||
| It's not a documented primitive, so there are no guarantees about it being available in a future version of the C API. | ||
| It _is_ part of the Python ABI, though, so it inherits the ABI's stability guarantees even though it carries no API guarantees. | ||
| Sometimes symbols are exposed like this for technical reasons, sometimes for historical reasons, and sometimes because a CPython user requested the ability to do something and was OK with the lack of API stability. |
There was a problem hiding this comment.
I'd omit the "because a CPython user requested" -- that's nowadays all "historical reasons". Or mistakes (not every core dev knows how this should work, and there's no police).
| While public symbols in the CPython C API can be removed following a deprecation period or to fix a serious defect, this only happens after a period of public discussion and consideration of the community impact. Critically, CPython has a policy not to add or remove items in the version-specific API from the first beta release of a Python minor version onward. | ||
| For example, this year Python 3.15.0b1 came out in May and that release froze the Python 3.15 version-specific C API. | ||
| All subsequent betas, release candidates, and official releases share the same set of API items, including function signatures and struct layouts, with no changes allowed until the next Python minor release. | ||
| This also means the ABI is frozen. |
There was a problem hiding this comment.
This does not follow -- the ABI chan change independently of the ABI.
| This also means the ABI is frozen. | |
| The ABI is also frozen when the first release candidate of a new minor version is released. |
| As of 2025, during the Python 3.15 development cycle, there were still a few more spots that needed updating: | ||
|
|
||
| - [A new C API](https://peps.python.org/pep-0793/) for defining modules that avoids `PyModuleDef`, a type that extends `PyObject`. | ||
| - [An accompanying new API](https://peps.python.org/pep-0820/) for defining Python types and modules using a common `PySlot` struct. |
There was a problem hiding this comment.
PEP 820 wasn't needed; I think this post can safely omit it. (PEP 793 should get notes about the API change, and of course the docs don't mention PEP-793-without-820 at all).
| imageAlt: 'Five nested ellipses illustrating the layering of the Python C API. The outermost ellipse is gray and labeled "Internal API". The next enclosed ellipse is red and is labeled "Private API". The next enclosed ellipse is yellow and is labeled "Unstable API". The next enclosed ellipse is blue and labeled "Version-specific API". The next enclosed ellipse is green and is labeled "Limited API".' | ||
| --- | ||
|
|
||
| # What Every Python Developer Should Know About the CPython ABI |
There was a problem hiding this comment.
The title might be too clickbaity? You shouldn't need to care until you want to build extension modules...
|
|
||
| A platform ABI determines things like the exact way machine code needs to pass arguments to a function. For example, on the [`x86_64`](https://wiki.osdev.org/System_V_ABI#x86-64) architecture, only the first six non-floating-point arguments of a function are passed via registers, the remaining arguments are passed via the stack. | ||
| Normally, developers do not need to worry about details like this: compilers automatically generate machine code appropriate for whatever platform ABI a developer wants to target. | ||
| However, it _is_ important to know that different operating systems and CPU architectures have unique ABIs and that code compiled for one platform ABI is completely unusable on another platform ABI. |
There was a problem hiding this comment.
I've been using the term “the (underlying) platform” for this. Maybe you can too?
| However, it _is_ important to know that different operating systems and CPU architectures have unique ABIs and that code compiled for one platform ABI is completely unusable on another platform ABI. | |
| However, it _is_ important to know that the underlying platform -- details like | |
| different operating systems and CPU architectures -- have unique ABIs and that code compiled for one platform ABI is completely unusable on another platform ABI. |
This blog post contains content that will go into my EuroPython talk.
I'm adding it as a blog post to help me structure my thoughts for the talk and so marketing has a post to share about this topic.
Text styling
Non-text contents