PEP 803: Stable ABI for Free-Threaded Builds (packaging thread)

What should installers do when all 3 of abi3 abi3t and abi3.abi3t (compressed tag) wheels are available?

I would imagine nobody should be uploading all 3 of those in the “ideal end state”, but I wouldn’t be surprised that some people may at least build all 3 simply to test that the intersection of these APIs has identical or acceptably similar behavior from end user perspectives.

With the current state of freethreading, even if this is accepted, at least for the time being, I intend to continue uploading separate wheels for abi3 and abi3t rather than rely on the intersection as I share some concerns expressed above that there may be unintended issues and would like to further test this before pushing that to downstream users.

In that state, I may build all 3, and there may even be a useful reason to upload all 3 to allow others to test, but in the presence of all 3, I would prefer defaulting to the one that matches the host rather than the compressed tag, but I’m actually not sure if this is well-defined already in terms of what installers should do, if it’s left up to installers explicitly, or not defined and left to installers by lack of standard.

I believe the answer based on current installer logic (specifically pip’s but it’s built into how packaging reports compatibility, which in turn is based on how the packaging tags spec was designed) is that the installer will pick the wheels that work with the preferred set of supported tags. That will be either abi3 and abi3.abi3t, or abi3t and abi3.abi3t, depending on whether the target environment returns abi3 or abi3t first.

Of the two wheels, I think the installer is allowed to pick either, on the assumption that they will be functionally identical. And I think that’s correct - all existing wheels that use compressed tags are considered functionally identical to a wheel that uses just a single one of the compressed tag set. So abi3/abi3t should work the same way.

Edit:

One of these two (which only differ in terms of the intent you want to ascribe to not standardising behaviour in this case).

2 Likes

Do something similar to what they do when cp315, cp315t, and cp315.cp315t wheels are available. I don’t think the specs say what to do here, so maybe use a set and pick a different one each time depending on how the tags are hashed?

This is not a new way to entertain your users with “fun” reproducibility issues :‍)

1 Like

Good enough to answer the questions I had there, thank you both. I couldn’t find if installer resolution for compressed tags had a stronger specified rule for preference already, if the existing case is that it isn’t specified more strongly and this is just following the limits of what is already specifed here, that’s clear enough for me.

So on my end, if I build all 3, I should stick to uploading either than 2 separate ones or the 1 that is intended to cover both once I’m more confident in shipping that to users, and keep the others available elsewhere for those interested, rather than on index if I want predictable “typical end users should get this”

I think the closest thing to a “standard” on what a installer should do when presented with multiple wheels that are all compatible with a given environment is a general vague suggestion to pick the “most specific” one.

That mostly comes into play with separate wheels rather than compressed tags, so I’m not sure that anyone has thought about it in particular to compressed tags, but the standard is vague anyways so :slight_smile:

Thanks. I skimmed over that and it didn’t “click” in my head because the opt-in mechanisms are kind of confusingly named to me. This is largely due to history, both GIL and FT have a Limited API, but Py_LIMITED_API only opts you into the GIL limited API (again, history) and Py_TARGET_ABI3T is what opts you into the FT limited API.

Maybe it’d make sense to deprecate Py_LIMITED_API and add Py_TARGET_ABI3 as well (Or maybe treat Py_LIMITED_API as an alias to whichever Py_TARGET_ABI* is native for the “current” Python) to make it clearer– but that’s just bikeshedding, so feel free to ignore it.

I agree that ideally GIL and FT are hopefully converging. Where I think there’s risk is that we’re trying to treat them like they’ve already converged.

There’s a simple guideline in my head that I think makes sense? Roughly speaking, I think that a given Python interpreter should only attempt to import an extension module for an ABI that it understands, with *.so being an wildcard ABI (so everything understands it).

The rule for “don’t load .abi3.so on FT Python” flows from that– FT Python is not compatible with .abi3.so so it shouldn’t load it.. and your PR does that, which is great I appreciate that!

The sticking point is for .abi3t.so, which I think is a sticking point because currently both GIL and FT Python do understand it, but we’re allowing ourselves the capability to make it so that GIL Python does not.

I think where we’re missing each other on abi3t is that we’re approaching it from opposite angles.

I think your thinking here is that hopefully we won’t need to diverge abi3t, and so this gives us all of the benefits of not diverging, while keeping our options open and minimizing the cost. If we don’t diverge, then that ends up working out great, GIL Python continues to understand abi3t and thus importing it isn’t an issue.

I’m approaching it from the angle of “we’re giving ourselves the ability to diverge, so what happens if we actually do that?”. The answer in the PEP (with and without the current PR) I think is “well now GIL Python is loading an abi3t extension even though it doesn’t understand abi3t in this hypothetical future where they’ve diverged”.

Feels kind of like the difference between optimistic and pessimistic concurrency control :slight_smile:

My worry is that we can ask that tools update themselves, but there’s no forcing function to actually require that they do, and if they do there’s no forcing function to require that their users actually use the updated version of their tool. It’ll just silently produce the wrong thing.

If we treat abi3.abi3t as undefined (so it’s on tools to figure out what to do, just like we treat cp314.cp315 as undefined), there’s nothing stopping us from deciding in the future that GIL and FT have converged and now abi3t also covers GIL natively. However, if we treat it such that GIL Python can load .abi3t.so when it doesn’t actually understand abi3t, we can’t undo that. It’ll have to continue to load .abi3t.so extensions into the future or we break all of the existing wheels.

I’m still struggling to understand what the downside of having abi3.abi3t wheels use a bare .so is.

As far as I can tell, the tagged .so files are only useful if you have overlapping installation directories and you’re relying on the .so tags to allow different Python interpreters to pick the correct one.

That doesn’t generally apply to the wheel ecosystem, so a bare .so is “just as good”.

It feels like we’re trying to contort the tagged .so to support something that only makes sense in the wheel ecosystem, when the tagged .so’s aren’t even required or very useful for the wheel ecosystem to begin with. In doing that, we’re making them less useful in the places where they are required and they are useful.

Or to put another way, having GIL Python load *.abi3t.so when we’re explicitly saying that it might not actually support abi3t in the future means that, in that future, the systems that need the tagged .so files can’t rely on those tags anyways.

What benefit does having GIL Python load *.abi3t.so files give us, other than the ability to have a abi3.abi3t wheel with a single extension file in it– which can also be achieved with just *.so, which preserves the ability for those systems to rely on the .so tags, while having no downsides (that I can think of) for the wheel ecosystem.

1 Like

I spent yesterday and this morning implementing a version of @dstufft’s proposal in a set of packaging tools. To explore the dual compatibility story a bit better, I made it so abi3t wheels are loadable on the GIL-enabled build and there is no abi3.abi3t or any kind of combined build that ships both extensions. There are only abi3 wheels that contain extensions with .abi3.so suffixes that only work on the GIL-enabled build, or abi3t wheels that contain extensions with .abi3t.so suffixes that work on both builds.

My goal is to check whether this maximalist version of the proposal that people seem to be worried about will actually cause issues in real-world projects.

My fork of setuptools currently requires a free-threaded interpreter to generate an abi3t wheel. It’s possible to add UI for this as well, although projects may need to manually define Py_GIL_DISABLED for such builds, setuptools doesn’t set any compiler defines.

All of this is based on the private _Py_OPAQUE_PYOBJECT macro, which will presumably be replaced by Py_TARGET_ABI3T or similar if PEP 803 is accepted.

tl;dr

I tested a variety of build tools and real-world projects that plan to or currently ship abi3 wheels. The only significant issue I had in any project was with astropy, since it ships static types and static module definitions that will need to be updated, so it cannot easily support the new limited API right now.

Other than that, I have not seen any crashes or deadlocks on either build using an .abi3t.so file extension and a cp315-abi3t wheel tag with no compressed ABI tag sets.

CPython patch

I have a very small patch for CPython that updates importlib.machinery.EXTENSION_SUFFIXES and adds a test as well:

The way I set it up, the GIL-enabled build loads both extensions with .abi3.so and .abi3t.so suffixes. I haven’t yet stress-tested scenarios where both builds are available in the same package, but I think the sensible thing is to prefer .abi3.so if both are found. I think that’s actually how it works in my branch, I just haven’t added any integration tests to prove that. I also found that Meson is senstive to the ordering of this list, see below.

Stable ABI Test Harness

If you build CPython from my branch and want to experiment with builds using the abi3t ABI, you’ll also need to do something analogous to this series of pip install commands to install various forks of projects that need patching to support opaque PyObject builds:

NO_CYTHON_COMPILE=true python3.15t -m pip install git+https://github.com/cython/cython.git@freethreading-limited-api-preview
python3.15t -m pip install git+https://github.com/ngoldbaum/pip.git@abi3t
python3.15t -m pip install git+https://github.com/ngoldbaum/packaging.git@abi3t
python3.15t -m pip install git+https://github.com/ngoldbaum/setuptools.git@abi3t
python3.15t -m pip install git+https://github.com/ngoldbaum/meson-python.git@abi3t
python3.15t -m pip install git+https://github.com/ngoldbaum/cffi.git@abi3t
python3.15t -m pip install git+https://github.com/kumaraditya303/numpy.git@opaque --no-build-isolation

The diffs for the packaging tools are all pretty small. If anything, this approach without the compressed ABI tag set is simpler, because there’s no need to teach build backends about the existence of compressed ABI tag sets.

That said, while typing this post I noticed that Meson will also maybe need to be patched to produce .abi3t.so extensions on the GIL-enabled build, since it uses a static index into EXTENSION_SUFFIXES to find the correct .so file name for limited API builds. If we made the GIL-enabled interpreter prefer .abi3t.so extensions the current code would also work correctly, but I don’t think we want that.

@mgorny and I have been working on a set of tests for packaging tools in this repo: GitHub - Quansight-Labs/stable-abi-testing: Projects to test the upcoming the Python stable ABI changes · GitHub . It builds extensions with a matrix of build tools, programming languages, and binding generators, with a goal of sniffing out incompatibility with PEP 803. The extensions are very simple, they just expose an add function that adds two C ints. It verifies that the extension builds correctly, with the correct ABI tag in the wheel file, and verifies that the extension can be successfully imported and executed on both builds, if you happen to have both builds available in your PATH.

Currently the stable ABI tests target the version of PEP 803 from before @dstufft pointed out the issues with Debian and shared installs, so I have a branch that updates everything:

All the tests pass on my Mac dev machine with CPython builds from my branch. You can run the build with tox -e py. You can run it with either build or with both on your PATH.

Note that the only “real” tests are the ones using meson-python and setuptools. We still need to update scikit-build-core and maturin. Support in PyO3 will also need to wait until PyCriticalSection and related API land in the stable ABI.

Testing with real-world packages.

I also spent some time this morning testing out @steve.dower’s worries about using abi3t extensions on the GIL enabled build. Are the code blocks in Py_GIL_DISABLED blocks in extensions problematic on the GIL-enabled build?

With the limited testing I did today, I didn’t find any issues so far.

Pywavelets

Pywavelets currently ships cp314t wheels and has been worked on already to support the free-threaded build. I was able to successfully build a cp315-abi3t wheel based on @rgommers’ branch. The pywavelets tests all pass on both builds. I also tried testing with pytest-run-parallel and did not see any deadlocks or other issues. All tests that pytest-run-parallel does not automatically mark as thread-unsafe pass in parallel. Pywavelets doesn’t have any explicitly multithreaded stress tests, so this is all testing for global state in the Cython-generated code as well as trying to assess if there are any mutexes or other possibly problematic types in Py_GIL_DISABLED blocks generated by Cython. That said, pywavelets also doesn’t define any mutable types in its extensions, so it wouldn’t be obvious what to test.

cymem

cymem is a small Cython utility library that provides a memory pool type via a Cython .pxd header file. Our team worked on it earlier this year. It’s at the bottom of the dependency stack for spaCy. It’s a nice small example that also has nontrivial code to handle thread safety on the free-threaded build. In particular, it has a few critical sections.

@lys.nikolaou wrote a multithreaded test for cymem to validate thread safety. When I buidl an abi3t wheel on the free-threaded build, the test passes on the GIL-enabled build when I install the wheel. [1]

brotlicffi

Brotlicffi recently started shipping cp314t wheels. There is a multithreaded test that checks to make sure concurrent use leads to errors (libbrotli is not thread-safe). I needed to make a very small patch to enable limited API builds. Because the brotlicffi build uses my fork of setuptools, you need a free-threaded interpreter to build an abi3t wheel. Allowing abi3t builds on the GIL-enabled interpreter will require some new UI in setuptools and CFFI, I think.

As far as I can tell, everything works on both builds when I install from the same abi3t wheel and run the brotlicffi tests.

yt

The yt project is an analysis and visualization tool. Once upon a time it was my day job to help maintain it. I learned this week that yt ships limited API wheels. To my surprise, everything seems to work. Note that no one has done any work to validate thread safety in yt and it does not yet support the free-threaded build. That means when I tried running the yt tests under pytest-run-paralell, I saw some thread safety issues due to global state in yt’s implementation. I didn’t see any deadlocks or crashes on either build.

This also required updating ewah-bool-utils, which yt depends on, to build abi3t wheels. I didn’t see any issues in that library either.

astropy

astropy also ships abi3 wheels. However, astropy does not rely entirely on Cython for C extensions, it also has some pure C extensions that define static types and use PyModuleDef_HEAD_INIT to initialize a few modules. I commented on the issue an astropy developer already opened about Python 3.15 stable ABI support. In the process, I also found extension-helpers, which astropy uses to help manage Cython extension builds. That project will also need a small patch to work with the new stable ABI.


  1. Note that I’m working around the issue I identified with meson while writing this by building the wheel on the free-threaded build. I need to work out with the Meson and meson-python developers what the proper way to thread the correct extension name down to meson is. ↩︎

12 Likes

That’s reassuring, thank you for the effort and for writing it all up in such detail.

I haven’t done anything to update it, but the main difference in my PEP 809 compared to this proposal[1] was to just define a single ABI that’s the same for both FT and GIL builds, leaving the current abi3 only for GIL builds and encouraging all package developers to migrate to the new stable ABI if they want to support FT. It sounds like that would still be feasible, and fundamentally is going to be the same as letting the GIL build support both abi3 and abi3t[2].

If it’s as viable to do this as Nathan has shown, then is there more support for using a name other than abi3t and framing it as “the next-gen stable ABI” rather than “the free-threading specific ABI”?


  1. Apart from the dynamic interfaces feature. ↩︎

  2. The critical piece of the proposal is to rename abi3t to something with longevity. ↩︎

3 Likes

Thank you Nathan for the real-world validation!


Yeah, but renaming configuration macros is a pain, and renames are always confusing in the short term.
My plan is to deprecate it after GIL-enabled build is removed, and Py_LIMITED_API is an alias for Py_TARGET_ABI3T.[1]

I think where we miss each other is that you think the extension’s ABI is named by the filename tag. I don’t think that’s the case in general (it already lacks the version number for abi3 extensions); for me it’s simply a mechanism to allow installing several extensions in a single directory.
We can’t (practically) get to a state where the filename tag is “authoritative”; but we can get closer to it. We disagree on how close we should get.

Yeah, that’s where we miss each other in a “what are you even talking about” way.
abi3 is defined as a set of symbols (with attached semantics). If abi3t is defined as another set of symbols (with same/analogous semantics), you don’t get to declare that their intersection is undefined.

Technically, with any possible variant of “stable ABI for free-threaded builds” you can build an extension that’s compatible with both CPython 3.15+ (free-threaded) and 3.15+ (GIL-enabled). The issue is how to name that. The wheel tag for that is clear; and the filename tag ⸺

that’s where we disagree.
Honestly, I don’t really care about the filename. To me, it’s a detail. But alas, I should care – and the PEP should care. Please just tell me what (and why) to do here, so I can review it & put it in; you don’t need to convince me that it’s the right tag to use :‍)
AFAIK, the filename tag is – on the technical level I care about – only relevant to Debian, so whatever we do that works for Debian (and doesn’t break others) is fine. Go ahead and look at it from your viewpoint (which I respect but struggle to fully understand), and let’s do what you see.

OK! I’d be perfectly happy with recommending bare .so for abi3.abi3t extensions!
(For the record, the current draft PR implements your previous suggestion: [private link]; I never said no to bare .so.)

There’s a surprising implication to using bare .so.
I’ll push for abi3.abi3t (not abi3t) to replace abi3 when GIL-enabled builds go away, because if it does remains the same as abi3t, it’ll be more compatible with no downsides.
In this preferred future of mine, bare .so will become the default. Support for bare .so is simple to implement in build tools, whose authors might notice it also works in all other cases except in Debian, and will need to weigh supporting Debian system package builds against implementation complexity.
Bare .so, sets me on a path against PEP 3149; I didn’t want to end up there.

Anyway, yeah, from the technical point of view, bare .so for abi3.abi3t totally works, and is a nice simple solution. However sad that is personally, it’s a winner.

To be clear, I won’t be pushing this (but will review if someone comes up with a proposal). It’s out of scope for this PEP. I’m struggling (= trying) to see the issues people have with the tag abi3t; in the time that’s left I can’t redesign the thing and get the superficial-to-me aspects right.


  1. “Native” target doesn’t make sense to me – if your build tool doesn’t know what that is, it won’t be able to generate correct metadata. It might work as a build tool option. ↩︎

The confusion I see is that the term abi3 is being used in multiple ways. And in this particular context I’m not even sure which way people are meaning :slightly_frowning_face:

I know the following could be viewed as nitpicking, but in this case I think the details are important - after all, the whole problem is that “you know what I mean” isn’t true…

The platform tags abi3 and abi3t aren’t defined in terms of a set of symbols, they are defined much more operationally - a platform supports the tag abi3 if it says it does, and a wheel should only use the tag abi3 if it will run on a platform that supports abi3. In that context, abi3.abi3t is well-defined as a wheel tag, meaning “runs on a platform that supports either abi3 or abi3t”. There’s nothing about symbol sets or build options involved here, except in the sense that build tools should choose what tags to apply by default[1] based on the build options chosen.

On the other hand, .so names also use the strings abi3 and abi3t to mean something. I’m not 100% sure what, exactly - my impression is that they are disambiguators to allow multiple .so files to exist in the same directory, with Python interpreters preferring one or another based on the rules built into the import system. In that context, .abi3.abi3t is only defined if the import system says it’s defined.

There’s another aspect of the wheel tag situation, which is that it’s up to us (i.e., the PEP) to define what systems should claim to support which of abi3 and abi3t wheel tags. The PEP as it currently stands isn’t at all clear on this (because it’s framed in terms of what “installers should do” rather than what “platforms should say”[2]) but it strongly suggests that free threaded interpreters should say they accept abi3t, and GIL-based interpreters should say they accept abi3. This gives a very clear interpretation of what platforms wheels tagged with abi3, abi3t, or abi3.abi3t should mean - abi3 is GIL-based only, abi3t is free threading only, and abi3.abi3t is either.

But I think what @dstufft is referring to is a .so file extension of .abi3.abi3t, and not wheel tags? If that is the case, then I have no view on what we decide - how the interpreter loads .so files has no impact on packaging standards. Although I can understand (and agree with) his argument here. Overlapping installation directories are not covered by the packaging standards on how wheels are installed, and so the packaging ecosystem simply doesn’t care about disambiguation. As far as wheels (and hence wheel build tools) are concerned, just using a bare .so extension is 100% fine. ABI-tagged .so files are only needed for custom, non-wheel builds by distributors who are taking advantage of the fact that we support overlapping installations in the interpreter. So it’s only they who need tagged .so files (and they don’t typically use Python’s packaging tools to install them, they have their own installers and distribution formats).

I will note that PEP 803 as currently written has an error. In “Recommendations for build tools” it says:

Such extensions should be tagged with the compressed tag set abi3.abi3t.

But compressed tag sets are a concept that’s part of the wheel tag specification, and as such you don’t tag extensions with them. Rather, you create (one or more) extensions with a .so file type, and bundle them in a wheel tagged with compatibility tags.

Maybe what should be said here is something along the lines of

If all of the extensions contained in a wheel are compiled to be compatible with both abi3 and abi3t, the wheel should be tagged with the abi3.abi3t compatibility tag. If any extensions are compiled to be only compatible with abi3, the wheel must be tagged as abi3 only. Similarly for abi3t. Wheels containing a mix of abi3-only and abi3t-only extensions are invalid and must not be created.

The question of what name to give an extension built to be compatible with abi3 or abi3t (or both) is a core interpreter question, and should not be in the section on “the abi3t wheel tag”. Not least because if I’m compiling a C extension by hand, and not using a wheel building tool at all, I still need that information.


  1. “default”, because the user can always rename the wheel… ↩︎

  2. the latter is, I believe, a much clearer framing ↩︎

1 Like

To me, abi3 (and abi3t) is a compatibility guarantee and a set of API/ABI limitations. Wheels with extensions that meet the limitations (and enjoy the guarantee) signal this by using the name as a tag, and installers use (complicated) logic to determine if the compatibility guarantee is in effect for a given interpreter. They don’t define what abi3 is; they use the name.
(Similar for an interpreters “accepting” abi3 – the interpreter will, essentially, run any code you give it, though there are a few safeguards in place – like the filename tag. Of course, it is good to make these safeguards more useful, by better matching the underlying limitations/guarantees.)

Filenames can contain a tag too, but they can’t signal both abi3 & abi3t. If you need to leave some info out, then (the lack of) a filename tag can’t always be used to signal (in)compatibility.

That leaves some design space around what the wheel & filename tags should mean exactly. There’s no single correct solution; tell me what works best :‍)

I don’t think so? There’s no cp314.cp315 filename tag, so this wouldn’t make sense:

But word lawyering gets us nowhere. Donald, if this is still relevant, could you clarify? :‍)

I think that’s way too lawyery. AFAIK, current standards don’t (and shouldn’t) say what to do if a wheel combines abi3 & cp314 extensions, or that combining cp313 & cp314 extensions is invalid[1]. So I think I’ll go with:

Wheels with such extensions should be tagged with the compressed tag set abi3.abi3t.

Core core doesn’t care; importlib cares but gives you (too many) options. IMO, recommendations for build tools is the right section.
(You’d use bare .so when building by hand when you don’t care about distribution – that is, about signaling compatibility guarantees – but that’s out of scope here.)


  1. Is that invalid? AFAICS, that would be a “fat” wheel that supports both 3.13 and 3.14, and presumably uses uses either PEP 3149 filename tags, or somehow carefully sticking to the intersection of the ABIs. You can do the same with mix of abi3-only and abi3t-only extensions. (That’s why the section is recommendations – the “normal”, easy, thought-through way to meet the underlying limitations & compatibility guarantees. There’s a lot of space to be creative.) ↩︎

1 Like

I’ve updated the draft PR: abi3t extsnsions should use .abi3t.so extensions; abi3.abi3t extensions should use bare .so.

2 Likes

Here, the Meson maintainer is strongly against using bare .so: meson#15637

Yes, your last change doesn’t quite work for build systems and seems unnecessary. It’s hard to keep up with the volume of posts this week, but my understanding is that

  1. The reuse of .abi3.so was always a pragmatic shortcut, and seemed fine.
  2. Now that it’s no longer fine, because Debian, using .abi3t.so instead of .abi3.so is a clean change that addresses the concern Stefano and Donald raised. Some extra work to change build systems, but nothing dramatic - and the design is still unambiguous.
  3. No other change seemed needed or warranted. The discussion went all over the place, but there’s no compelling reason as far as I can tell to change what you had:
    • The compressed packaging tags work fine
    • A GIL-enabled interpreter can be taught to load .abi3t.so. A lot of work was done to make this possible. The only concern was Steve’s gut feel that it’d be hard, but that was hopefully addressed by Nathan’s work.

The rationale for using a plain .so seems missing, and it would make the work for build systems harder. I’d revert that change in your PR.

3 Likes

I agree. The PR is back to .abi3t.so meaning abi3+abi3t.
It also reorganizes/rewords the rationale & rejected ideas, and hopefully makes things more clear and consistent overall.

1 Like

Hi @encukou and @ngoldbaum ,

On behalf of the Steering Council, I am happy to share that PEP 803 has been approved. Congratulations, and thank you for your work on this PEP.

We are particularly pleased to see this align well with the expectation expressed in the PEP 779 acceptance post that Stable ABI for free-threading should be prepared and defined for Python 3.15.

This is a meaningful step toward that goal, and we are glad to see the free-threading work continue on the right path.

Warm regards,
Donghee
on behalf of the Python Steering Council

12 Likes

I reposted the same message there for visibility:

Thank you @da-woods for catching this.