From d569c4c6041d2625ca1b34dfc982586af1f43324 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 01:15:54 +0800 Subject: [PATCH 01/17] Add failback implementation of `PyCriticalSection_BeginMutex` for Python 3.13t --- include/pybind11/detail/internals.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index b9b0f08f27..fc54699792 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -245,7 +245,11 @@ class pycritical_section { public: explicit pycritical_section(pymutex &m) : mutex(m) { +# if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); +# else + _PyCriticalSection_BeginMutex(_PyThreadState_GET(), &cs, &mutex.mutex); +# endif } ~pycritical_section() { PyCriticalSection_End(&cs); } From 5e7d42369291cedd8f232f66ee69cf006ebaa1fa Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 01:19:30 +0800 Subject: [PATCH 02/17] Add comment for Python version --- include/pybind11/detail/internals.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index fc54699792..6fa9f22379 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -245,7 +245,8 @@ class pycritical_section { public: explicit pycritical_section(pymutex &m) : mutex(m) { -# if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 + // PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 +# if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); # else _PyCriticalSection_BeginMutex(_PyThreadState_GET(), &cs, &mutex.mutex); From 9de6d1a78c6ad82f014c663b6785428290a39b16 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 03:46:54 +0800 Subject: [PATCH 03/17] Use `_PyCriticalSection_BeginSlow` --- include/pybind11/detail/internals.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 6fa9f22379..80c4b885da 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -249,7 +249,8 @@ class pycritical_section { # if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); # else - _PyCriticalSection_BeginMutex(_PyThreadState_GET(), &cs, &mutex.mutex); + // Use the slow path of internal API `_PyCriticalSection_BeginMutex` for older versions + _PyCriticalSection_BeginSlow(&cs, &mutex.mutex); # endif } ~pycritical_section() { PyCriticalSection_End(&cs); } From 00bcf662371d8ab29fd36a19834fb2a53eb33048 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 03:59:02 +0800 Subject: [PATCH 04/17] Add forward declaration --- include/pybind11/detail/internals.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 80c4b885da..0fe64d8fba 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -239,6 +239,11 @@ class pymutex { void unlock() { PyMutex_Unlock(&mutex); } }; +# if !(defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 /* 3.14.0rc1 */) +// Forward declaration of internal slow path function for usage in pycritical_section +void _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m); +# endif + class pycritical_section { pymutex &mutex; PyCriticalSection cs; @@ -246,7 +251,7 @@ class pycritical_section { public: explicit pycritical_section(pymutex &m) : mutex(m) { // PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 -# if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 +# if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 /* 3.14.0rc1 */ PyCriticalSection_BeginMutex(&cs, &mutex.mutex); # else // Use the slow path of internal API `_PyCriticalSection_BeginMutex` for older versions From 225cb46c0e403ae63bba2841317679be3f08b94b Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 04:13:50 +0800 Subject: [PATCH 05/17] Fix forward declaration --- include/pybind11/detail/internals.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 0fe64d8fba..86ed851a7b 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -241,7 +241,7 @@ class pymutex { # if !(defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 /* 3.14.0rc1 */) // Forward declaration of internal slow path function for usage in pycritical_section -void _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m); +extern "C" void _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m); # endif class pycritical_section { From 0e6ffd3fa2506f1db5ab933bb3e6c751fe3b33b7 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 04:16:14 +0800 Subject: [PATCH 06/17] Remove always true condition `defined(PY_VERSION_HEX)` --- include/pybind11/detail/internals.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 86ed851a7b..274bda054e 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -239,7 +239,7 @@ class pymutex { void unlock() { PyMutex_Unlock(&mutex); } }; -# if !(defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 /* 3.14.0rc1 */) +# if PY_VERSION_HEX < 0x030E00C1 // 3.14.0rc1 // Forward declaration of internal slow path function for usage in pycritical_section extern "C" void _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m); # endif @@ -251,7 +251,7 @@ class pycritical_section { public: explicit pycritical_section(pymutex &m) : mutex(m) { // PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 -# if defined(PY_VERSION_HEX) && PY_VERSION_HEX >= 0x030E00C1 /* 3.14.0rc1 */ +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); # else // Use the slow path of internal API `_PyCriticalSection_BeginMutex` for older versions From 24db64d8953745e9a6143e2b476a4fe36fb9462e Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 19:01:59 +0800 Subject: [PATCH 07/17] Detect musllinux --- tests/env.py | 12 ++++++++++++ tests/test_multiple_interpreters.py | 1 + 2 files changed, 13 insertions(+) diff --git a/tests/env.py b/tests/env.py index 8c06178307..790a0108fc 100644 --- a/tests/env.py +++ b/tests/env.py @@ -11,6 +11,18 @@ WIN = sys.platform.startswith("win32") or sys.platform.startswith("cygwin") FREEBSD = sys.platform.startswith("freebsd") +MUSLLINUX = False +MANYLINUX = False +if LINUX: + + def _is_musl() -> bool: + libc, _ = platform.libc_ver() + return libc == "musl" or (libc != "glibc" and libc != "") + + MUSLLINUX = _is_musl() + MANYLINUX = not MUSLLINUX + del _is_musl + CPYTHON = platform.python_implementation() == "CPython" PYPY = platform.python_implementation() == "PyPy" GRAALPY = sys.implementation.name == "graalpy" diff --git a/tests/test_multiple_interpreters.py b/tests/test_multiple_interpreters.py index 56d303a36e..ce04ee00d4 100644 --- a/tests/test_multiple_interpreters.py +++ b/tests/test_multiple_interpreters.py @@ -408,6 +408,7 @@ def test_import_in_subinterpreter_before_main(): @pytest.mark.skipif( sys.platform.startswith("emscripten"), reason="Requires loadable modules" ) +@pytest.mark.xfail(env.MUSLLINUX, reason="Flaky on musllinux", strict=False) @pytest.mark.skipif(not CONCURRENT_INTERPRETERS_SUPPORT, reason="Requires 3.14.0b3+") def test_import_in_subinterpreter_concurrently(): """Tests that importing a module in multiple subinterpreters concurrently works correctly""" From c17212a4931fc418ad8dfd86a1ed98c4001c3b1f Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 19:42:34 +0800 Subject: [PATCH 08/17] Add manylinux test --- .github/workflows/ci.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0965fa813..24d143f259 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -237,11 +237,19 @@ jobs: manylinux: - name: Manylinux on 🐍 3.14t + name: Manylinux on 🐍 ${{ matrix.python-version }} (${{ matrix.container }}) if: github.event.pull_request.draft == false + strategy: + fail-fast: false + matrix: + include: + - container: quay.io/pypa/manylinux_2_28_x86_64:latest + python-version: '3.14t' + - container: quay.io/pypa/musllinux_1_2_x86_64:latest + python-version: '3.14t' runs-on: ubuntu-latest timeout-minutes: 40 - container: quay.io/pypa/musllinux_1_2_x86_64:latest + container: ${{ matrix.container }} steps: - uses: actions/checkout@v6 with: @@ -254,7 +262,7 @@ jobs: run: uv tool install ninja - name: Configure via preset - run: cmake --preset venv -DPYBIND11_CREATE_WITH_UV=python3.14t + run: cmake --preset venv -DPYBIND11_CREATE_WITH_UV="python${{ matrix.python-version }}" - name: Build C++11 run: cmake --build --preset venv From 3d4af6097a2b65731e2d2560780d0f2018b407ec Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Tue, 3 Feb 2026 20:37:27 +0800 Subject: [PATCH 09/17] Use direct mutex locking for Python 3.13t `_PyCriticalSection_BeginSlow` is a private CPython function not exported on Linux. For Python < 3.14.0rc1, use direct `mutex.lock()`/`mutex.unlock()` instead of critical section APIs. --- include/pybind11/detail/internals.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 274bda054e..b681529325 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -239,14 +239,11 @@ class pymutex { void unlock() { PyMutex_Unlock(&mutex); } }; -# if PY_VERSION_HEX < 0x030E00C1 // 3.14.0rc1 -// Forward declaration of internal slow path function for usage in pycritical_section -extern "C" void _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m); -# endif - class pycritical_section { pymutex &mutex; +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection cs; +# endif public: explicit pycritical_section(pymutex &m) : mutex(m) { @@ -254,11 +251,17 @@ class pycritical_section { # if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); # else - // Use the slow path of internal API `_PyCriticalSection_BeginMutex` for older versions - _PyCriticalSection_BeginSlow(&cs, &mutex.mutex); + // Fall back to direct mutex locking for older free-threaded Python versions + mutex.lock(); +# endif + } + ~pycritical_section() { +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 + PyCriticalSection_End(&cs); +# else + mutex.unlock(); # endif } - ~pycritical_section() { PyCriticalSection_End(&cs); } // Non-copyable and non-movable to prevent double-unlock pycritical_section(const pycritical_section &) = delete; From 358d3c76e5f2905b70aaa985be4c13591d8698de Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Wed, 4 Feb 2026 19:50:37 +0800 Subject: [PATCH 10/17] Empty commit to trigger CI From fea12dc6ea6d65bb2ab1818d9727955d5c6f56e9 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Thu, 5 Feb 2026 00:20:07 +0800 Subject: [PATCH 11/17] Empty commit to trigger CI From 1ef8caedcbdbc4785df0cd045a7c1fac20a8ca95 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Thu, 5 Feb 2026 14:51:43 +0800 Subject: [PATCH 12/17] Empty commit to trigger CI From f7a002c4aef59174551f6227975e881f42967f85 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Thu, 5 Feb 2026 16:38:10 +0800 Subject: [PATCH 13/17] Run apt update before apt install --- .github/workflows/reusable-standard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 56d92e2779..6e22d0f38b 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -44,7 +44,7 @@ jobs: - name: Setup Boost (Linux) if: runner.os == 'Linux' - run: sudo apt-get install libboost-dev + run: sudo apt-get update && sudo apt-get install -y libboost-dev - name: Setup Boost (macOS) if: runner.os == 'macOS' From 3f82060e66b93465c40198c8a573883b4c2ff02b Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Thu, 5 Feb 2026 17:33:55 +0800 Subject: [PATCH 14/17] Remove unnecessary prefix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24d143f259..0fd99ed83a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -262,7 +262,7 @@ jobs: run: uv tool install ninja - name: Configure via preset - run: cmake --preset venv -DPYBIND11_CREATE_WITH_UV="python${{ matrix.python-version }}" + run: cmake --preset venv -DPYBIND11_CREATE_WITH_UV="${{ matrix.python-version }}" - name: Build C++11 run: cmake --build --preset venv From f20f126cee3361c2eb49a31600c50d1013e97ed3 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Thu, 5 Feb 2026 21:09:45 +0800 Subject: [PATCH 15/17] Add manylinux test with Python 3.13t --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fd99ed83a..03cb0238cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -243,6 +243,10 @@ jobs: fail-fast: false matrix: include: + - container: quay.io/pypa/manylinux_2_28_x86_64:latest + python-version: '3.13t' + - container: quay.io/pypa/musllinux_1_2_x86_64:latest + python-version: '3.13t' - container: quay.io/pypa/manylinux_2_28_x86_64:latest python-version: '3.14t' - container: quay.io/pypa/musllinux_1_2_x86_64:latest From 89b2879808f051bf79e52b4bbafacd9842981d08 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 6 Feb 2026 00:12:32 +0800 Subject: [PATCH 16/17] Simplify pycritical_section with std::unique_lock fallback for Python < 3.14 --- include/pybind11/detail/internals.h | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index b681529325..5131272c71 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -230,7 +230,9 @@ using instance_map = std::unordered_multimap; #ifdef Py_GIL_DISABLED // Wrapper around PyMutex to provide BasicLockable semantics class pymutex { +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 friend class pycritical_section; +# endif PyMutex mutex; public: @@ -239,29 +241,17 @@ class pymutex { void unlock() { PyMutex_Unlock(&mutex); } }; +// PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 class pycritical_section { pymutex &mutex; -# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection cs; -# endif public: explicit pycritical_section(pymutex &m) : mutex(m) { - // PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 -# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); -# else - // Fall back to direct mutex locking for older free-threaded Python versions - mutex.lock(); -# endif - } - ~pycritical_section() { -# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 - PyCriticalSection_End(&cs); -# else - mutex.unlock(); -# endif } + ~pycritical_section() { PyCriticalSection_End(&cs); } // Non-copyable and non-movable to prevent double-unlock pycritical_section(const pycritical_section &) = delete; @@ -269,6 +259,9 @@ class pycritical_section { pycritical_section(pycritical_section &&) = delete; pycritical_section &operator=(pycritical_section &&) = delete; }; +# else +using pycritical_section = std::unique_lock; +# endif // Instance map shards are used to reduce mutex contention in free-threaded Python. struct instance_map_shard { From 9a039dfdec9c344018fdf7db36a1126d760e0243 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 6 Feb 2026 02:14:07 +0800 Subject: [PATCH 17/17] Fix potential deadlock in make_iterator_impl for Python 3.13t Refactor pycritical_section into a unified class with internal version checks instead of using a type alias fallback. Skip locking in make_iterator_impl for Python < 3.14.0rc1 to avoid deadlock during type registration, as pycritical_section cannot release the mutex during Python callbacks without PyCriticalSection_BeginMutex. --- include/pybind11/detail/internals.h | 23 +++++++++++++++-------- include/pybind11/pybind11.h | 4 ++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 5131272c71..b681529325 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -230,9 +230,7 @@ using instance_map = std::unordered_multimap; #ifdef Py_GIL_DISABLED // Wrapper around PyMutex to provide BasicLockable semantics class pymutex { -# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 friend class pycritical_section; -# endif PyMutex mutex; public: @@ -241,17 +239,29 @@ class pymutex { void unlock() { PyMutex_Unlock(&mutex); } }; -// PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 -# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 class pycritical_section { pymutex &mutex; +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection cs; +# endif public: explicit pycritical_section(pymutex &m) : mutex(m) { + // PyCriticalSection_BeginMutex was added in Python 3.15.0a1 and backported to 3.14.0rc1 +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PyCriticalSection_BeginMutex(&cs, &mutex.mutex); +# else + // Fall back to direct mutex locking for older free-threaded Python versions + mutex.lock(); +# endif + } + ~pycritical_section() { +# if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 + PyCriticalSection_End(&cs); +# else + mutex.unlock(); +# endif } - ~pycritical_section() { PyCriticalSection_End(&cs); } // Non-copyable and non-movable to prevent double-unlock pycritical_section(const pycritical_section &) = delete; @@ -259,9 +269,6 @@ class pycritical_section { pycritical_section(pycritical_section &&) = delete; pycritical_section &operator=(pycritical_section &&) = delete; }; -# else -using pycritical_section = std::unique_lock; -# endif // Instance map shards are used to reduce mutex contention in free-threaded Python. struct instance_map_shard { diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index f88fc20272..0f31262c47 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -3173,7 +3173,11 @@ iterator make_iterator_impl(Iterator first, Sentinel last, Extra &&...extra) { using state = detail::iterator_state; // TODO: state captures only the types of Extra, not the values + // For Python < 3.14.0rc1, pycritical_section uses direct mutex locking (same as a unique + // lock), which may deadlock during type registration. See detail/internals.h for details. +#if PY_VERSION_HEX >= 0x030E00C1 // 3.14.0rc1 PYBIND11_LOCK_INTERNALS(get_internals()); +#endif if (!detail::get_type_info(typeid(state), false)) { class_(handle(), "iterator", pybind11::module_local()) .def(