Repository navigation
Description
It is anticipated that Python 3.15 will support a version of the Limited API that means the same extension will be importable in both regular and free-threading Python. Some of the details are still to be decided but Cython aims to support this.
To let you start testing this early I've pushed a freethreading-limited-api-preview branch to this repo. This branch is intended to be fairly short-lived and will be deleted once it's all merge into the repo. The branch will probably not be kept rigorously up-to-date with other Cython bug fixes
It's a merge of the following PRs and branches:
- Add thread safety guards in Limited API 3.15 #7382
- Support opaque PyObject in cyfunction/fusedfunction #7190
- Implement PEP793 initialization #7333
- Support opaque objects for internal types (defaults and closure classes) #7412
- Opaque objects for coroutines/generators/async utility code #7410
- https://github.com/da-woods/cython/tree/cdef-class-opaque-pyobject
- Cherry-pick e03bc10 (to allow marking a module as freethreading_compatible)
- Some memoryview-handling stuff (probably to be picked separately) e.g. 87d9cea, 6d0af5b,
- Extern types 2c86e5f (probably more)
- master...da-woods:cython:defer_dealloc_in_freethreading or some similar workaround
Be aware that it is missing some some bits of internal thread safety which would be provided by critical sections and PyMutexes since these have not yet been added to the Limited API.
To use
Define the following CFLAGS when you build:
-DPy_LIMITED_API=0x030F0000 -DCYTHON_OPAQUE_OBJECTS=1 -D_Py_OPAQUE_PYOBJECT
Build with as recent alpha of Python 3.15 as you can.
Set up your build system normally so that the modules are named abi3.so (see https://cython.readthedocs.io/en/latest/src/userguide/limited_api.html#building-with-the-limited-api for some hints)
Known Limitations
Anything involving an extern type (cdef extern class or similar) is unlikely to work.
Performance is about 13% slower compare with the Py_LIMITED_API=0x030E0000 - essentially every access to an attribute of a cdef class goes through a painful indirect lookup.
Some thread-safety guards missing right now.
Activity
da-woods commentedon Dec 12, 2025
One detail that I've subsequently spotted is that it breaks our memoryview ABI compatibility. That's going to need either some workarounds or some checks.
da-woods commentedon Dec 13, 2025
The other outstanding issue is access to cdef classes without the GIL. I've currently implemented a very hacky workaround, but there's no guarantee it'll keep working long-term and Python developers are sceptical of it.
mgorny commentedon Jan 5, 2026
I've noticed that the generated C file has:
rather than:
Is this intentional?
da-woods commentedon Jan 5, 2026
@mgorny thanks for testing.
I think I deliberately didn't add it in #7382 because I don't think it should be in Cython master until we have access to PyMutex and critical section.
However, I think it should be available on this "preview" branch because hopefully everyone is sensible enough not know it's for testing and not for shipping. I'll push a commit to fix that shortly.
mgorny commentedon Jan 5, 2026
Yeah, I agree. Thanks!
da-woods commentedon Jan 18, 2026
I've pushed a mid-sized update to this.
I'll probably push another update later this week to try to clean up some memoryview stuff, time permitting. That'll be more code quality rather than any change in what works.
ngoldbaum commentedon Jan 22, 2026
Over in #7476 (comment), @da-woods pointed out that defining wrappers for PyObject subtypes like NumPy does in its Cython headers will need some work on the Cython side.
NumPy currently does something like this:
Maybe NumPy's cython headers will need some updates?
da-woods commentedon Feb 1, 2026
I've pushed some improvements to the handling of extern types (e.g. numpy's
numpy.ufuncobject). You can now specify:to explicitly specify whether the extern type opaque or not. It defaults to
Falseso there's no need to specify it for existing code.This means that non-opaque Cython modules can use opaque extern types (at least partly).
Opaque Cython modules don't check the size of non-opaque extern types. Largely because it will definitely be a compile error if you try to access their attributes, so it's "relatively" safe.
I imagine some of this will end up changing, so don't rely too much on it in this exact form. But hopefully it lets people get a bit further with the Numpy headers.
ngoldbaum commentedon Feb 23, 2026
@da-woods I'm planning to add a section to PEP 803 that quotes ecosystem library maintainers to comment on why PEP 803 will be useful. Can you comment a bit about why it'll be beneficial for Cython users? I can then excerpt that for the PEP.
No worries if you're not comfortable doing that.
da-woods commentedon Feb 23, 2026
@ngoldbaum I don't mind being quoted but.... I don't fully understand the appeal of the Stable ABI.
Cython is doesn't have huge problems with the number of wheels we distribute because ultimately it works fine as pure-Python. We do distribute wheels for a few of the smaller platforms as Stable ABI wheels but that's more "dogfooding" than because we actually need to. So I'm adding this in anticipation that other people will find it useful rather than because I will.
I do remain slightly concerned that the performance trade-offs for this will turn out to be too much for many Cython users (it's possible that the trade-off may be different for other binding tools). That's not a huge disaster since we're not getting rid of the regular compilation mode so people are free to pick their own personal trade-offs.
But none of that makes a particularly useful quote...
ngoldbaum commentedon Feb 24, 2026
I think it was useful! I opened python/peps#4831.
scoder commentedon Feb 25, 2026
Our benchmarks report our Limited API code (the
L-columns) as visibly slower than our optimised CPython code, e.g.https://github.com/cython/cython/actions/runs/22303110980
Most of the big-gap ones are micro-benchmarks, but from the rest, especially object intensive benchmarks like "richards" suffer by factors. They are often slower than the non-compiled pure Python code (the column right of it). The proposed Stable ABI changes are likely to make this relation even worse, and thus completely useless for anything performance relevant. [EDIT: I was referring to code where the performance of the C-API code matters, not one-shot long running native data processing where it does not.]
I can see the "external library wrapping" use case still being a candidate for the Stable ABI, even with the new worse performance. There are certainly cases where people say "I need the functionality, regardless of the speed". PyQT comes to my mind as a reasonable example, also audio/video processing libraries etc. that just need a "configure and run". There are certainly cases like this. But in many such cases, running a subprocess will be hugely easier and faster than connecting to a library through the Limited API.
I can't say whether it's really worth building and supporting a whole second API construct just for this use case. Performance matters for a lot of users in the Python ecosystem, and this isn't just about some "~10% YMMV". Anything for which the wrapping performance matters will probably try hard to avoid the Stable ABI at all cost.
rgommers commentedon Feb 25, 2026
I am not sure this take is correct in general. It seems to be true when one is using lots of short, micro-second level operations. When working with larger array data (quite common for numerical/scientific libraries), the differences quickly seem to converge to <=10%. See for example benchmarks for PyWavelets (which uses a lot of Cython code, and some C code) in PyWavelets/pywt#828. Scikit-learn (which is even more heavy on Cython usage) benchmarks are in the works, I'd expect roughly similar outcomes.
My expectation is that for the most popular platforms, well-maintained scientific libraries are likely to continue to take the wheel building pain to avoid any performance regressions, but for niche platforms it'll be either "no wheels" or "stable ABI wheels". And for less well-maintained libraries, the tradeoffs are leaning even more towards the stable ABI being useful.
20 remaining items
scoder commentedon Jun 25, 2026
We're not planning to integrate this for 3.3, see #7753 (comment)
It's a large pack of changes in various corners and I do not want to risk further extending the time that it'll take us to get the pile of new features in 3.3 to our users.
I just read up on the main parts of PEP-803, which seems to be the one that got the final blessing. It left me with the impression that independent ABIs for GIL- and noGIL-Python, and a temporal limit of the stability guarantees, seem the best path forward. As we see with abi3, we've come to a point where we can't hold up the stability guarantees – and that's fine, isn't it? Design a new replacement ABI, rebuild the packages, move on. Once a project has gone through the entire hassle to adopt one Stable ABI, the next ABI adaptation will probably be mostly a rebuild, with some fixes. Projects could still ship cross-Pyversion wheels for several years, thus reducing the total number of wheels considerably, and eventual EOL-driven reduction should then keep it mostly constant over time. We'd have abi3 for Py<=3.14(non-t), abi3 and abi1t for Py3.15+, and abi4 and abi2t eventually for, say, Py3.19 or Py4.3.
The main reason why I favour independent ABIs for GIL- and noGIL-Python targets is that I found it worth adapting the code independently for the two, e.g. considering locks vs. GIL-safety, so targeting both with a single wheel seems counter-intuitive to me, at least in most cases. Why enter a critical section when the code is guarded by the GIL? There's probably enough difference between both targets to merit independently optimised wheels, and so separate ABIs are unlikely to hurt much.
encukou commentedon Jun 25, 2026
Sure. The new noGIL one happens to be compatible with the GIL, though. (Perhaps not surprising: for one, the GIL can be re-enabled; also, if the ABI should ideally be compatible with “any” future implementation, making it incompatible with the known past implementations would be rather questionable.)
That would be nice, if we could predict when the next feature needs to break the guarantees.
Instead, CPython's ABI for the new feature got a new name.
In a GIL interpreter, critical sections are no-op functions in the stable ABI, and nothing (empty
#defines) in version-specific ABI.So, no difference until you care about the cost of a C function call, at which point you don't want the stable ABI anyway.
da-woods commentedon Jun 25, 2026
I broadly agree that if we want to do 3.3 soon then there's too many charges here. It does essentially rewrite every attribute access to cdef classes so there's a lot to go wrong.
I don't think the "independent ABIs" is meaningfully going to happen (or at least I was told that there was no chance of a free-threading ABI with non-opaque objects, which would be the main performance improvement).
As Petr (just) said, not omitting critical sections at compile time shouldn't cost a lot.
nitzmahone commentedon Jun 25, 2026
Definitely understand (and appreciate) the cautious approach!
IIUC
abi3talways ==abi3_abi3t, so the talk of separate paths above confused me (or I'm misinformed). Just to clarify: at least for 3.3 as-currently-planned,abi3tis not available- onlyabi3limited API builds will be available- free-threading support continues to require version-specific builds, yeah?da-woods commentedon Jun 25, 2026
Yes. That's right.
scoder commentedon Jun 25, 2026
I didn't mean to base it on prediction. When there is a good reason to change the ABI, then a decision can be taken in which (next or future) release to make that change. That release would then be the end of the current Stable ABI and the start of a new one, pretty similar to what is now happening with PEP-803. It would be a tradeoff decision between the cost of package maintenance/distribution and the cost of CPython development / language benefits / whatever the reason for the ABI change will be. Just another tradeoff, like so many others. I'm not convinced that making the object struct opaque is necessary, not right now, possibly not even in the future. It seems a premature pessimisation to me.
encukou commentedon Jun 26, 2026
That's the general plan on CPython side too -- with specifics left to be decided based on the necessary change.
Others saw non-opaque
PyObjectas the biggest problem ofabi3. It's not likely that any new stable ABI will go back to it.scoder commentedon Jun 26, 2026
I had skimmed through that issue, and sure, making the object struct opaque is one of the options. Whether it's a good one depends on which side of the ABI you stand on.
The issue's problem statement specifically refers to implementing immortal object in CPython in Py3.11/12. If a new Stable ABI4 based on new, optimised object struct layouts had been defined at that point, it could already have served for four release series since then, probably even beyond Py3.15(non-t). Some 4-6 years is a reasonable time frame for a stability guarantee that most maintainers and distributors could probably live with.
Opaque object structs are really a political decision, not a technical necessity.
dalcinl commentedon Aug 22, 2026
@scoder I'm not sure whether this is the best place to report, but looks like the following is a trivial code generation bug?
The following patch fixes the error above, but...
... but a new error pops up:
da-woods commentedon Aug 22, 2026
That looks like it's missing the
fto make it an fstring. I'll push a fix in a bit. I don't think cleanup_func is used much so obviously isn't well-testedda-woods commentedon Aug 22, 2026
It's worth bearing in mind that it doesn't look to mix hugely well with module state (#7905) - access to
cdef classattributes won't work outside the GIL. That probably won't be fixed soon (because the work needs to happen on the module-state side rather than the freethreading limited API side). So what you're testing may be disappointing.Either way, the immediate fix is pushed.
dalcinl commentedon Aug 23, 2026
@da-woods After your fix, my first try with my project (mpi4py) was a complete success. Too good to be true? I'll double check everything.
However, there is a minor issue with the code generated with this branch. If you have Cython code using the special
__weakref__attribute to support weak references, and then you compile WITHOUTPy_LIMITED_API/Py_TARGET_ABI3T(in other words, with CYTHON_OPAQUE_OBJECTS turn off), then C code dealing with the access of__weakref__in parent/child classes is broken.Here you have a minimal reproducer:
And this is the error attempting to compile that code with cythonize.py
Looks like a simple scope lookup issue?offsetoffshould use the parent class that defines__weakref__.EDIT: No, it is not that simple. The
entry.cnameno longer include the__pyx_base.prefix, that's why the currently generated C code fails.Add test for inherited weakref
da-woods commentedon Aug 27, 2026
Fixed
dalcinl commentedon Aug 28, 2026
@da-woods Thanks! Now I can confirm that everything is working OK with mpi4py. The ABI3T build passes all tests with both the GIL-enabled or the free-threaded interpreter. The generated C code builds fine with non-ABI3 from Python 3.10 to 3.15.