Skip to content

feat: support the Python stable ABI (abi3 and abi3t) - #6198

Open
henryiii wants to merge 45 commits into
pybind:masterfrom
henryiii:stable-abi
Open

henryiii wants to merge 45 commits into
pybind:masterfrom
henryiii:stable-abi

Conversation

@henryiii

@henryiii henryiii commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Only build one wheel for 3.12+ and 3.15t+ per platform and support all future versions of Python.

Implemented with Fable, added abi3t, got numpy working, got chrono working, iterated with linked Claude sessions on several downstream projects (finding both fixes and performance optimizations to keep this from being to costly), simplified with Fable, and reviewed with Astra, which found five issues. This was originally following #6104, with a commit for each "PR" in that roadmap, but there was quite a bit of iteration, so I opened this as one PR. I'm thinking it might be preferred to review this way these days anyway.

This supports abi3t on 3.15+ and abi3 on 3.12+. Original performance was pretty bad, but optimized a bit and some parts are faster than the unstable ABI (though those optimizations can of course be applied there too later). It depends very much on what you are doing (some things are slower with stable ABI, like attribute access with no call), but for most projects, this should be performant enough. awkward showed a 1-3% drop for one microbenchmark, otherwise unmeasurable. After fixes, iminuit shows no measurable slowdown (one microbenchmark is 17% faster due to an optimization to method-descriptor calls we could also apply to non-stable ABI builds).

Questions:

  • Should we auto-detect and use scikit-build-core's cmake variables?

🤖 AI text below 🤖

Description

Adds support for the CPython stable ABI (Py_LIMITED_API >= 0x030C0000, abi3) and the free-threaded stable ABI (abi3t, CPython 3.15+, PEP 803). One *.abi3.so per platform then runs on every CPython from the targeted version on.

Draft: opened so that others can build their projects against it. Tried so far on boost-histogram, iminuit, and awkward. PyTorch can't use it, due to custom code calling C-API functions, but checked to make sure it still works with the mode turned off.

How to try it:

  • CMake: pybind11_add_module(example STABLE_ABI example.cpp) or -DPYBIND11_STABLE_ABI=ON (CMake 3.26+, FindPython mode). PYBIND11_STABLE_ABI_VERSION selects the targeted version (default 3.12; 3.15 on free-threaded Python).
  • setuptools: Pybind11Extension(..., py_limited_api=True).
  • Any other build: define Py_LIMITED_API=0x030C0000 and name the module *.abi3.so.

What changes:

  • Type objects are created from PyType_Spec with PyType_FromMetaclass(). This path is also available to regular builds on 3.12+ as the PYBIND11_TYPE_CREATION_VIA_SPEC opt-in. (Edit: now opt-out)
  • Field reads of CPython structs go through public functions (PyType_GetSlot(), PyObject_GetTypeData(), checked tuple/list accessors, __mro__/__dict__ lookups).
  • Methods are bound through a pybind11 instancemethod type, which is a vectorcall method descriptor, so obj.method(...) does not create a bound method.
  • chrono.h uses the datetime types by attribute; numpy.h under abi3t needs NumPy 2.5+.
  • Not available under the stable ABI: embed.h, subinterpreter.h, py::custom_type_setup, GIL-held assertions; py::metaclass() is restricted. See docs/advanced/stable_abi.rst for the full table.
  • Stable-ABI and regular modules in one process do not share internals (the internals ID gets a _stable / _stable_ft tag); pybind11_conduit_v1 bridges them.

CI builds and runs the test suite for abi3 on 3.12 to 3.14 and abi3t on 3.15t, and runs the 3.12 abi3 binaries on 3.13 and 3.14 and the 3.15t abi3t binaries on GIL-enabled 3.15.

Suggested changelog entry:

  • Support the CPython stable ABI (abi3, Py_LIMITED_API >= 0x030C0000) and the free-threaded stable ABI (abi3t, CPython 3.15+). Enable with STABLE_ABI in pybind11_add_module(), py_limited_api=True in Pybind11Extension, or by defining Py_LIMITED_API. Type creation from PyType_Spec is also available to regular builds as the PYBIND11_TYPE_CREATION_VIA_SPEC opt-in.
  • Type objects are now created with PyType_FromMetaclass() by default on CPython 3.12+. Define PYBIND11_TYPE_CREATION_VIA_SPEC=0 (CMake -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF) to restore the previous path.

📚 Documentation preview 📚: https://pybind11--6198.org.readthedocs.build/

@henryiii

henryiii commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

If this is better in smaller parts, there are some good candidates for that, including two bug fixes and a nice performance speedup:

🤖 AI text below 🤖

Yes. Beyond the method-descriptor patch, there are two real bug fixes and one large feature that do not depend on Py_LIMITED_API, plus a pile of prep refactors.

Bug fixes (strongest candidates)

  • type_is_managed_by_our_internals() cross-module fix (ad10b98, include/pybind11/detail/cpp_conduit.h). Master compares tp_new against this module's pybind11_object_new, which fails for types whose base came from another module. The branch switches to the registry lookup PyPy already used. No test was added on the branch, so it needs one before it ships alone.
  • pybind11_traverse/pybind11_clear under PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET on 3.13+ (part of 4d65cb2). Master unconditionally calls PyObject_VisitManagedDict/PyObject_ClearManagedDict on 3.13+, even when that macro puts the dict at tp_dictoffset and the type has no Py_TPFLAGS_MANAGED_DICT. The dict is then never visited or cleared by the GC, at best. I did not run this on master, so verify with a 3.13 build and that macro before filing.

Feature, independent of the stable ABI

  • Spec-based type creation / PyType_FromMetaclass default on 3.12+ (fbc9d96, 8f47ff2, c05d08d, a5f341a, 04459a5, e669ac1, 4d65cb2). PYBIND11_TYPE_CREATION_VIA_SPEC is a separate switch from Py_LIMITED_API and already has its own CI job. It is big, but it is the natural first PR: the stable ABI PR then shrinks to "the spec path, plus limited-API gating".

Method descriptor / instancemethod (the one you named)

  • 73cd296 and 721fabc give pybind11's own instancemethod type vectorcall and Py_TPFLAGS_METHOD_DESCRIPTOR. On the branch that type exists only under Py_LIMITED_API; master uses PyInstanceMethod_Type, which lacks the flag. A standalone PR has to make the own type unconditional. If you do that, 973e9ef comes along for free: the metaclass tp_getattro can be dropped everywhere, which removes the slow MRO walk on class attribute reads, and the interned-name type_lookup() speedup applies to regular builds too.

Prep refactors, low value alone but they shrink the main diff

  • Traceback walk by attribute in error_already_set::what() (683f376).
  • PyThreadState_GetInterpreter() instead of tstate->interp, with the PyPy fallback (5c50c3d, part of e0bc493).
  • get_bases()/get_mro()/get_tp_name()/tp_name_is_one_of() helpers (d1af605, 1865f0b) and the PyTuple_GET_ITEM-style macro replacements (cdbc9b4).
  • value_and_holder::type_is_exact() replacing the repeated Py_TYPE(v_h.inst) == v_h.type->type in init.h and pybind11.h.
  • The cross-module test modules factored into an exec function (test-only).

Not separable

  • The chrono per-TU fix (88fb22a) fixes helpers the branch itself introduced; master inlines the macros, so it does not apply.
  • SABIModule export (573d161), eval.h, numpy.h, complex.h, the TLS/critical-section changes, and the conftest compile-time version logic are stable-ABI-only.

Py_LIMITED_API is not supported yet; a build with it defined failed with
pages of unrelated errors. Add a single #error in common.h, and reserve a
bit for Py_LIMITED_API in the precompiled configuration check so later
stable-ABI work can switch it on.

See pybind#6104 (B1).

Assisted-by: ClaudeCode:claude-fable-5-1
…ULE_LOCAL_ID

Modules built with Py_LIMITED_API will use a different internals layout, so
they must not share internals with regular modules in one process. The tag
is empty today and becomes "_stable" under Py_LIMITED_API. It is not part
of PYBIND11_PLATFORM_ABI_ID, so cpp_conduit keeps bridging both worlds.

See pybind#6104 (B2).

Assisted-by: ClaudeCode:claude-fable-5-1
…p_bases/tp_mro

Direct reads of tp_bases/tp_mro are not possible under the Python stable
ABI. Route all reads through two small helpers so the limited-API branch
can be added in one place later.

See pybind#6104 (B3).

Assisted-by: ClaudeCode:claude-fable-5-1
…tead of raw tp_name reads

Direct reads of tp_name are not possible under the Python stable ABI.
obj_class_name() now returns std::string.

See pybind#6104 (B4).

Assisted-by: ClaudeCode:claude-fable-5-1
Comparing tp_new against this module's pybind11_object_new fails for types
whose pybind11_object base was created by another module (the inherited
pointer belongs to that module's copy). The registry lookup, which was the
PyPy path, is correct across modules and does not read type-object fields.

See pybind#6104 (B5).

Assisted-by: ClaudeCode:claude-fable-5-1
Read tb_next, tb_frame, f_back, co_filename and co_name through attribute
access and use the public PyFrame_GetCode/PyFrame_GetLineNumber functions,
instead of poking traceback, frame and code object struct fields, which are
not part of the stable ABI.

See pybind#6104 (B6).

Assisted-by: ClaudeCode:claude-fable-5-1
_PyType_Lookup is a private CPython function and is not available under
the stable ABI. Route the three users (metaclass getattro/setattro and the
cpp_conduit instance-method check) through one helper.

See pybind#6104 (B7).

Assisted-by: ClaudeCode:claude-fable-5-1
PyThreadState is opaque under the stable ABI; the accessor function is
public since Python 3.9.

See pybind#6104 (B8).

Assisted-by: ClaudeCode:claude-fable-5-1
… fallbacks

Gate the PyPy fallbacks that exist because cpyext does not expose CPython
struct layouts (static-property __set__, bool conversion, tuple/list fast
iterators) on one semantic macro. The stable ABI needs the same fallbacks.

See pybind#6104 (B9).

Assisted-by: ClaudeCode:claude-fable-5-1
Use the function forms (available under the stable ABI) at the non-hot
sites, and route the dispatcher and tuple/list caster sites through small
detail:: wrappers that keep the macro forms when struct access is available.

See pybind#6104 (B10).

Assisted-by: ClaudeCode:claude-fable-5-1
The macro will select PyType_FromMetaclass-based type creation on CPython
3.12+. It is a no-op for now; this adds the switch, its precompiled
config-check bit, a CMake option, and a CI matrix that builds with it on.

See pybind#6104 (C1).

Assisted-by: ClaudeCode:claude-fable-5-1
The metaclass and static-property implementations called the tp_* fields
of PyType_Type and PyProperty_Type directly. Look the slots up once per
module with PyType_GetSlot(), which works on static types since 3.10 and is
part of the stable ABI; older versions and PyPy keep the direct reads.

See pybind#6104 (C2).

Assisted-by: ClaudeCode:claude-fable-5-1
…ON_VIA_SPEC

Create the static-property type with PyType_FromMetaclass() when the
opt-in macro is defined. The __dict__ getset is shared with the legacy
path through dynamic_attr_getset().

See pybind#6104 (C3).

Assisted-by: ClaudeCode:claude-fable-5-1
…N_VIA_SPEC

See pybind#6104 (C4).

Assisted-by: ClaudeCode:claude-fable-5-1
…IA_SPEC

Weak-reference support moves from tp_weaklistoffset to a
__weaklistoffset__ member, which is how PyType_Spec expresses it.

See pybind#6104 (C5).

Assisted-by: ClaudeCode:claude-fable-5-1
…_VIA_SPEC

Bound classes are created with PyType_FromMetaclass(): bases are passed
as a tuple, the docstring through Py_tp_doc (no manual tp_doc allocation),
dynamic attributes through Py_TPFLAGS_MANAGED_DICT plus traverse/clear
slots, the buffer protocol through Py_bf_* slots, and py::is_final by
omitting Py_TPFLAGS_BASETYPE. __qualname__ is set after creation.

The hand-filled path stays as make_new_python_type_legacy() and is still
used for py::custom_type_setup, metaclasses with a custom tp_new, and a
py::metaclass less derived than pybind11_type, which PyType_FromMetaclass
refuses.

See pybind#6104 (C6).

Assisted-by: ClaudeCode:claude-fable-5-1
type_alloc()/type_free() use PyType_GetSlot() when direct slot access is
unavailable; instance_dict_ptr() is the single place that reads the
instance dictionary pointer.

See pybind#6104 (C7).

Assisted-by: ClaudeCode:claude-fable-5-1
Replace the Py_LIMITED_API #error with real gates and the missing pieces:

- Require CPython 3.12+, reject PyPy/GraalPy and free-threaded builds,
  force PYBIND11_SIMPLE_GIL_MANAGEMENT and PYBIND11_TYPE_CREATION_VIA_SPEC,
  disable subinterpreter support; embed.h and chrono.h #error, and
  py::custom_type_setup is unavailable.
- Heap-allocated Py_tss_t keys (the struct is opaque).
- dynamic_attr types and pybind11_static_property append the __dict__ slot
  to the base layout (no Py_TPFLAGS_MANAGED_DICT); the offset is cached in
  type_info because _PyObject_GetDictPtr is unavailable.
- A pybind11-provided instancemethod type (shared through the internals)
  replaces PyInstanceMethod_Type; bound methods go through types.MethodType.
- Attribute-based fallbacks for tp_name, __bases__/__mro__, type lookup,
  frame inspection, docstrings on PyCFunctions, dict.setdefault, length
  hints, generators, complex numbers, and eval/exec (Py_CompileString).
- The import-time version check accepts any interpreter from the
  Py_LIMITED_API version on.

Modules built this way run unchanged on every CPython from the targeted
version on. See docs/advanced/stable_abi.rst (next commits) for the
feature matrix.

See pybind#6104 (D1).

Assisted-by: ClaudeCode:claude-fable-5-1
pybind11_add_module(... STABLE_ABI) builds the module against the Python
stable ABI through python_add_library(USE_SABI) (CMake 3.26+, FindPython
mode) and names it *.abi3.so / *.pyd. PYBIND11_STABLE_ABI makes it the
default for a build tree and NO_STABLE_ABI opts a target out;
PYBIND11_STABLE_ABI_VERSION (default 3.12) selects the targeted version.
SHARED helper libraries get the same Py_LIMITED_API definition. The
classic (FindPythonLibs) mode reports a clear error.

pybind11_precompile(STABLE_ABI) compiles the precompiled library against
the limited API; one build tree cannot mix stable-ABI and regular
precompiled modules.

The test suite gains PYBIND11_TEST_STABLE_ABI, which builds every test
module as abi3 and skips the features that the stable ABI cannot offer.

See pybind#6104 (D2).

Assisted-by: ClaudeCode:claude-fable-5-1
py_limited_api=True (or a version such as "3.13") defines Py_LIMITED_API
for the targeted version in addition to setuptools' abi3 file naming, so
one wheel serves every CPython from that version on.

See pybind#6104 (D3).

Assisted-by: ClaudeCode:claude-fable-5-1
Build every test module as abi3 on Linux, macOS, and Windows (one job also
with the precompiled library), and run the modules built against the 3.12
headers unchanged on 3.13 and 3.14.

See pybind#6104 (D4).

Assisted-by: ClaudeCode:claude-fable-5-1
How to enable it with CMake, setuptools, or by hand; the feature matrix;
ABI isolation between stable-ABI and regular modules; and the
implementation differences that affect performance.

See pybind#6104 (D5).

Assisted-by: ClaudeCode:claude-fable-5-1
When the consumer sets Python_ARTIFACTS_INTERACTIVE before
add_subdirectory(pybind11), FindPython runs in pybind11's directory scope.
Python::SABIModule was not promoted to global and Python_SOSABI was not
cached, so STABLE_ABI modules either failed to configure or lost the
.abi3 suffix. Add a test_cmake_build case that covers this.

Assisted-by: ClaudeCode:claude-fable-5-1
Accept Py_LIMITED_API >= 0x030F0000 together with Py_GIL_DISABLED (PEP 803).
PyObject and PyModuleDef are then incomplete types, so pybind11:

- keeps `instance` and its other heap-type data as PEP 697 type data and
  reaches it through get_instance()/instance_object() instead of casts;
- exports modules through the PEP 793 PyModExport_<name> hook; the
  PYBIND11_MODULE macro is unchanged, create_extension_module() is gone;
- locks the internals with critical sections on a private object (PyMutex
  is not in the stable ABI) and keeps one weak reference per registered
  instance in place of PyUnstable_TryIncRef();
- tags the internals "_stable_ft", since abi3t modules also load on
  GIL-enabled interpreters.

numpy.h is not available under abi3t: it mirrors NumPy's object layouts.

CMake raises the stable ABI version to 3.15 on free-threaded Python and
Pybind11Extension(py_limited_api=True) does the same. The test suite is
built as .abi3t modules in CI for 3.15t and also run on GIL-enabled 3.15.

Assisted-by: ClaudeCode:claude-fable-5-1
Under abi3t the size of PyObject_HEAD is not known at compile time, so
the proxy structs drop it and the accessors go through the field getters
NumPy 2.5 added for the same purpose (_PyArray_GET_ITEM_DATA and
_PyDataType_GET_ITEM_DATA); void scalars are read through the buffer
protocol. Older NumPy versions are rejected at run time. The numpy and
Eigen tests run again under abi3t.

Assisted-by: ClaudeCode:claude-fable-5-1
The datetime C API is not part of the limited API. Under Py_LIMITED_API,
read the fields as attributes and build objects by calling the datetime
types; regular builds keep the C macros. The chrono test runs under the
stable ABI again.

Assisted-by: ClaudeCode:claude-fable-5-1
Attribute reads on bound classes (and Python subclasses of them) went
through pybind11_meta_getattro, which walks the MRO with attribute
calls and raised KeyError for every miss: ~20x slower than `type`.

pybind11's own instancemethod type already returns itself from
`__get__` when accessed on a class, so the metaclass does not need to
intercept reads. Drop the slot under Py_LIMITED_API and speed up the
remaining type_lookup() users (setattro, conduit) with interned names
and PyMapping_GetOptionalItem.

Assisted-by: ClaudeCode:claude-fable-5-1
henryiii and others added 17 commits October 10, 2026 14:47
Avoids the varargs collection in PyObject_CallFunctionObjArgs on every
`instance.method` access under the limited API (36 -> 32 ns here; the
regular build's PyMethod_New path is 22 ns).

Assisted-by: ClaudeCode:claude-fable-5-1
Collapse the ad hoc stable-ABI code paths into shared helpers:

- type_dictoffset/type_basicsize with cached interned names replace
  seven __dictoffset__/__basicsize__ reads; attr.h uses the cached
  type_info::dictoffset
- type_from_spec replaces five "PyType_FromMetaclass or fail" copies
- type_data<T> replaces the two PyObject_GetTypeData wrappers
- value_and_holder::type_is_exact replaces five alias checks
- tp_name_is_one_of fetches the type name once per candidate list
- PYBIND11_BASE_TYPE_SLOT defines whole functions (readable layout)
- chrono.h: one accessor macro per branch, interned field names
- getattr(obj, name, handle()) replaces hand-rolled lookups

Cheaper paths: type_lookup returns a borrowed handle with
_PyType_Lookup, the bool caster reads nb_bool via PyType_GetSlot under
the stable ABI, and PYBIND11_CHECK_PYTHON_VERSION compares Py_Version
instead of parsing Py_GetVersion().

Remove dead code: unused list_get_item, the unreachable try_incref stub
under PYBIND11_OPAQUE_PYOBJECT, the inert force-define of
PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET, a redundant PyGen_Check
branch and a dead preprocessor condition.

Assisted-by: ClaudeCode:claude-fable-5-1
ClaudeCode:claude-fable-5-1
A stable-ABI build targeting 3.12 with 3.13+ headers selected
PyDict_GetItemStringRef, PyDict_SetDefaultRef, PyEval_GetFrameBuiltins
and PyEval_GetFrameGlobals, which the headers hide below their limited
API version. Add PYBIND11_API_VERSION_HEX (Py_LIMITED_API when defined,
else PY_VERSION_HEX) and use it in those guards. annotations() always
takes the 3.14 path under the stable ABI, as the module can run there.

Assisted-by: ClaudeCode:claude-fable-5-1
With Py_TPFLAGS_METHOD_DESCRIPTOR and Py_TPFLAGS_HAVE_VECTORCALL (both
in the 3.12 stable ABI), `obj.method(...)` calls the instancemethod
with `obj` prepended instead of creating a types.MethodType first.
`obj.method` without a call is unchanged.

Release, CPython 3.12, `d.name()`: 77 ns regular, 45 ns abi3 (was 63).

Assisted-by: ClaudeCode:claude-fable-5-1
`PyDateTimeAPI` is a static per translation unit. The non-limited helpers
had external linkage, so the linker could pair one unit's
`datetime_import()` with another unit's `is_datetime()`, which then read
a null pointer. Give the helpers internal linkage and add a second
chrono translation unit to the test module.

Assisted-by: ClaudeCode:claude-fable-5-1
…spension

Regular free-threaded builds lock the instance-map shards with PyMutex
again, which is never suspended. Under abi3t only critical sections are
available; `find_registered_python_instance()` now takes strong
references under the shard lock and checks the types after releasing
it, since `all_type_info()` can suspend the section while waiting for
the internals lock.

Assisted-by: ClaudeCode:claude-fable-5-1
…ited API

CPython's `PyInstanceMethod_Type`, used in regular builds, does not set
`Py_TPFLAGS_METHOD_DESCRIPTOR`.

Assisted-by: ClaudeCode:claude-fable-5-1
`python_add_library(SHARED)` always links `Python::Python`. Create the
target with `add_library()` instead so the helper only depends on the
stable ABI.

Assisted-by: ClaudeCode:claude-fable-5-1
PyPy has `PyCFunction_GET_SELF` only as a macro and has no
`PyThreadState_GetInterpreter()`.

Assisted-by: ClaudeCode:claude-fable-5-1
Assisted-by: ClaudeCode:claude-fable-5-1
Add PYBIND11_ABI3T (CMake) and py_limited_api="3.15t" (setuptools) to
target abi3t with Py_TARGET_ABI3T from GIL-enabled CPython 3.15+, with
the .abi3t suffix and python3t.lib on Windows. Document Py_TARGET_ABI3T.

Use `auto it` in the tuple/dict iterator tests under Py_LIMITED_API (as on
PyPy), and build the cross-module RTTI bindings without the stable ABI so
they share internals with the embedding executable.

Assisted-by: ClaudeCode:claude-opus-5-5
…heck

Assisted-by: ClaudeCode:claude-opus-5-5
…thon 3.12+

PYBIND11_TYPE_CREATION_VIA_SPEC is now a 0/1 value that defaults to 1 on
CPython 3.12+ (and is forced on under Py_LIMITED_API). Define it as 0, or
configure with -DPYBIND11_TYPE_CREATION_VIA_SPEC=OFF, to keep the legacy
PyHeapTypeObject path. The CI job that used to opt in now opts out.

The spec path now honors PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET
(appending a `__dictoffset__` member instead of Py_TPFLAGS_MANAGED_DICT),
and pybind11_traverse/pybind11_clear no longer call the managed-dict
functions on 3.13+ when that macro is defined.

Assisted-by: ClaudeCode:claude-fable-5-1
ClaudeCode:claude-fable-5-1
wheel.py-api only selects the wheel tag; the CMake side must enable
STABLE_ABI from SKBUILD_SABI_COMPONENT and SKBUILD_SABI_VERSION.

Assisted-by: ClaudeCode:claude-fable-5-1
ClaudeCode:claude-fable-5-1
PyDict_SetDefaultRef is only in the limited API from 3.15, so a
Py_LIMITED_API=0x030E0000 build failed to compile. Alpha-suffixed
gates also never matched a limited API target, so 3.13 targets did
not use PyDict_GetItemStringRef. Add a 3.14-target CI build.

Assisted-by: ClaudeCode:claude-fable-5-1
ClaudeCode:claude-fable-5-1
@rwgk

rwgk commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

@henryiii this is the first pass reviewing with

codex GPT-6.1-Sol ultra:

Rechecked against 75be228. All six findings remain after the rebase:

  1. [P1] Pin registry candidates before acquiring the internals lock.
    get_object_handle() retains registry iterators and raw instances across values_and_holders(), which acquires the internals lock through all_type_info(). Under abi3t, contention suspends the outer registry critical section, allowing another thread to erase entries or rehash the shard. py::get_override() can then resume with invalid iterators or freed wrappers. Please collect strong candidates under the registry lock and inspect them afterward, as find_registered_python_instance() already does, retaining the selected wrapper through override lookup.

  2. [P2] Keep dictionary-offset helpers available for non-limited builds.
    Defining PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET in a normal CPython 3.12+ build now makes including pybind11.h fail to compile. The new spec path calls type_dictoffset and type_basicsize, but both helpers exist only under Py_LIMITED_API. I reproduced this with CPython 3.14.4 headers; the same include compiles without that macro. Please provide non-limited equivalents or retain direct field access for this configuration.

  3. [P2] Check base metaclasses before selecting the spec path.
    The fallback condition checks only the requested/default metaclass. Binding Base with py::metaclass(Meta), where Python defines class Meta(type): pass, selects the legacy path. Subsequently binding py::class_<Derived, Base>(m, "Derived") selects the spec path with the default pybind11_type. PyType_FromMetaclass() then raises a metaclass conflict; the previous PyType_Ready path accepts this binding. Please consider the base metaclasses when deciding whether to fall back.

  4. [P2] Recognize Windows free-threaded Python suffixes.
    The fallback detection matches cpython-…t, whereas Windows Python 3.15t uses cp315t-win_amd64 / .cp315t-win_amd64.pyd. Since Python_FREE_THREADED was added in CMake 4.4, CMake 3.30–4.3 misdetects this interpreter as GIL-enabled. Default STABLE_ABI consequently targets 3.12 and fails compilation because free-threaded stable ABI requires 3.15.

  5. [P2] Preserve FindPython paths for Windows abi3t consumers.
    With add_subdirectory(pybind11) and Python discovery inside that subdirectory, Python_INCLUDE_DIRS and Python_SABI_LIBRARY_DIRS remain scoped there. A caller using GIL-enabled Windows Python 3.15+, PYBIND11_ABI3T=ON, and pybind11_add_module(... STABLE_ABI ...) reaches the manual setup with empty paths. find_library(... NO_DEFAULT_PATH REQUIRED) fails even when python3t.lib is installed. Please obtain these paths from the global imported target or preserve them for callers.

  6. [P2] Include the second chrono translation unit when selecting chrono tests.
    -DPYBIND11_TEST_OVERRIDE=test_chrono excludes the independently listed test_chrono_second_tu.cpp, while test_chrono.py unconditionally imports its submodule. The extension builds, but pytest fails collection with ImportError. Please include the helper translation unit whenever chrono is selected.

Finding 2 was reproduced through compilation at this revision. The others are based on code inspection; no test suites were run.

- Pin registry candidates before values_and_holders() takes the internals
  lock in get_object_handle(); it now returns an owning object.
- Provide type_dictoffset()/type_basicsize() for non-limited builds so
  PYBIND11_BACKWARD_COMPATIBILITY_TP_DICTOFFSET compiles on 3.12+.
- Fall back to the legacy type-creation path when a base's metaclass is
  not compatible with the requested one.
- Detect Windows free-threaded SOABI (cp315t-win_amd64).
- Read the python3t.lib directory from the Python::SABIModule target.
- Include test_chrono_second_tu when PYBIND11_TEST_OVERRIDE selects test_chrono.

Assisted-by: ClaudeCode:claude-fable-5-1
ClaudeCode:claude-fable-5-1

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants