Skip to content

fix: support Python 3.14 #5646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 17, 2025
Merged

Conversation

henryiii
Copy link
Collaborator

@henryiii henryiii commented May 8, 2025

Description

Let's try beta 1!

Suggested changelog entry:

* Support Python 3.14 (beta 1)

@henryiii
Copy link
Collaborator Author

henryiii commented May 8, 2025

FAILED test_methods_and_attributes.py::test_dynamic_attributes - AssertionError: assert not True
 +  where True = hasattr(<pybind11_tests.methods_and_attributes.DynamicClass object at 0x7f3c250dd750>, 'foo')
FAILED test_operator_overloading.py::test_return_set_of_unhashable - assert False
 +  where False = <built-in method startswith of str object at 0x7f3c252b3030>('unhashable type:')
 +    where <built-in method startswith of str object at 0x7f3c252b3030> = "cannot use 'pybind11_tests.operators.HashMe' as a set element (unhashable type: 'pybind11_tests.operators.HashMe')".startswith
 +      where "cannot use 'pybind11_tests.operators.HashMe' as a set element (unhashable type: 'pybind11_tests.operators.HashMe')" = str(TypeError("cannot use 'pybind11_tests.operators.HashMe' as a set element (unhashable type: 'pybind11_tests.operators.HashMe')"))
 +        where TypeError("cannot use 'pybind11_tests.operators.HashMe' as a set element (unhashable type: 'pybind11_tests.operators.HashMe')") = TypeError('Unable to convert function return value to a Python type! The signature was\n\t() -> set[pybind11_tests.operators.HashMe]').__cause__
 +          where TypeError('Unable to convert function return value to a Python type! The signature was\n\t() -> set[pybind11_tests.operators.HashMe]') = <ExceptionInfo TypeError('Unable to convert function return value to a Python type! The signature was\n\t() -> set[pybind11_tests.operators.HashMe]') tblen=1>.value
FAILED test_pickling.py::test_roundtrip_with_dict[PickleableWithDict] - AttributeError: 'pybind11_tests.pickling.PickleableWithDict' object has no attribute 'dynamic'
FAILED test_pickling.py::test_roundtrip_with_dict[PickleableWithDictNew] - AttributeError: 'pybind11_tests.pickling.PickleableWithDictNew' object has no attribute 'dynamic'

@rwgk
Copy link
Collaborator

rwgk commented May 8, 2025

The tests/test_operator_overloading.py failure is a trivial fix, but I'm not sure about the other two. They could easily steal a couple hours each to take care of...

@henryiii
Copy link
Collaborator Author

henryiii commented May 9, 2025

Looks like two errors: py::hash isn’t working, and dynamic attributes are broken. @vstinner I don’t see anything in https://docs.python.org/3.14/whatsnew/3.14.html#id11 that would seem to indicate changes here.

I can try bisecting later.

@vstinner
Copy link
Contributor

vstinner commented May 9, 2025

  • where <built-in method startswith of str object at 0x7f3c252b3030> = "cannot use 'pybind11_tests.operators.HashMe' as a set element (unhashable type: 'pybind11_tests.operators.HashMe')".startswith

Yeah, I changed the error message in: python/cpython#132825

You may be able to fix your test by replacing "startswith" with "contains" ("in").

@henryiii
Copy link
Collaborator Author

henryiii commented May 9, 2025

Wow, that's a huge improvement:

Screenshot 2025-05-09 at 12 00 54 PM

Love the syntax highlighting in the REPL now, too. :)

I thought there was another issue with py::hash, but I see it's just that, so it's only the dynamic change now. I still don't see anything in the upgrade guide. But one issue should be easy to bisect.

@vstinner
Copy link
Contributor

vstinner commented May 9, 2025

I still don't see anything in the upgrade guide

The change was documented 1h ago: python/cpython@de28651.

@henryiii
Copy link
Collaborator Author

henryiii commented May 9, 2025

BTW, we are also running into https://gitlab.kitware.com/cmake/cmake/-/issues/26926 on Windows, CMake is confused and trying to use a free-threaded lib. I've seen that with scikit-build-core, too, in scikit-build/scikit-build-core#1074.

@henryiii
Copy link
Collaborator Author

henryiii commented May 9, 2025

Bisecting this shows it was broken by python/cpython#123192. I've commented on python/cpython#115776.

Edit: opened python/cpython#133912

@henryiii henryiii force-pushed the henryiii/chore/py314 branch from 0e89da3 to 2cd17ba Compare May 15, 2025 18:43
Comment on lines 18 to 21
if sys.version_info >= (3, 15):
import interpreters
elif sys.version_info >= (3, 13):
elif sys.version_info >= (3, 14):
import _interpreters as interpreters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like they didn't do this rename in 3.14, so it should be _interpreters for 3.13 and 3.14. Maybe just get rid of the 3.15 and leave it as :

    if sys.version_info >= (3, 13):
        import _interpreters as interpreters

Or future proof it as:

    if sys.version_info >= (3, 13):
        try: 
            import interpreters
        except ImportError:
            import _interpreters as interpreters

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there will be a pypi package for 3.14 eventually (though I think it was supposed to come to 3.13 originally, so who knows...), but let's just leave it as <3.15 for now.

@henryiii
Copy link
Collaborator Author

Is sys._is_immortal not working on Windows? It seems to be returning False. Also, I see a warning about the GIL being engaged due to a GIL extension we should investigate.

@henryiii henryiii marked this pull request as draft May 16, 2025 06:18
@henryiii
Copy link
Collaborator Author

henryiii commented May 16, 2025

It's not Windows specific, all of them report:

sys._is_immortal(cls)=False
sys.getrefcount(cls)=1152921504606846979

And exo_planet_c_api is re-engaging the GIL.

@henryiii henryiii force-pushed the henryiii/chore/py314 branch from 7d3aefb to 1f2e5d6 Compare May 16, 2025 14:24
@henryiii
Copy link
Collaborator Author

Can be reproduced in pure Python on 3.14t:

>>> import sys
>>> class Thing:
...     pass
>>> sys._is_immortal(Thing)
False
>>> sys.getrefcount(Thing)
1152921504606846980

Let's just ignore large (30 bits+) values.

@colesbury
Copy link
Contributor

colesbury commented May 16, 2025

In the 3.14 free threaded build, types, module-level functions, and some other objects use deferred reference counting. They have a large value (2^62 or something 2^60) added to their reference count so that it doesn't hit zero. These objects are not immortal; they can be collected during GC.

@colesbury
Copy link
Contributor

There are some more details here: https://peps.python.org/pep-0703/#deferred-reference-counting

There's a few things going on here:

  • Heap type objects aren't immortal in 3.14t, but they were immortal as a temporary measure in 3.13t
  • Heap type objects use a combination of deferred reference counting and per-thread reference counting. PEP 703 has a description of both. They have a very large reference count, but unlike immortal objects, the reference count isn't fixed and the objects can eventually be collected.
  • Instances don't directly incref their types in 3.14t 1 in order to avoid scaling bottlenecks due to reference count contention. They instead increment a counter in a per-thread array corresponding to the type. These "per-thread" reference counts aren't included in Py_REFCNT or sys.getrefcount() because safely doing so would require a stop-the-world pause. The GC takes them into consideration when deciding whether it can free a heap type object.

Footnotes

  1. This is specifically aboutPyObject_Init(). If you manually call Py_INCREF(type_obj) on a heap type object, it will directly modify the type's reference count.

@henryiii
Copy link
Collaborator Author

The test is just making sure pybind11 classes refcount correctly. I think it's fine for it to be checked to be either the correct value or a large value. Unless there's a way to "stop the world" and get the total recount?

@henryiii
Copy link
Collaborator Author

Subinterpreter issue on Windows 3.14t (CC @b-pass):

  <frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'widget_module', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
  <frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'widget_module', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
  <frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'widget_module', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
  <frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'widget_module', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
  <frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'widget_module', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
  <frozen importlib._bootstrap>:491: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'widget_module', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.
  
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  test_embed.exe is a Catch v2.13.10 host application.
  Run with -? for options
  
  -------------------------------------------------------------------------------
  Subinterpreter
  -------------------------------------------------------------------------------
  D:\a\pybind11\pybind11\tests\test_embed\test_interpreter.cpp(339)
  ...............................................................................
  
  D:\a\pybind11\pybind11\tests\test_embed\test_interpreter.cpp(339): FAILED:
    {Unknown expression after the reported line}
  due to unexpected exception with message:
    ImportError: module widget_module does not support loading in subinterpreters
  
  Assertion failed: t_int == main_int, file D:\a\pybind11\pybind11\tests\test_embed\test_interpreter.cpp, line 498

@henryiii
Copy link
Collaborator Author

Our single free-threaded test before didn't have support for embedded tests, so that's why this slipped in, I expect. I'm adding all the free-threaded Pythons to the CI.

@henryiii
Copy link
Collaborator Author

In the free threading build for 3.13t and 3.14t, this isn't working:

        // widget_module did not provide the mod_per_interpreter_gil tag, so it cannot be imported
        bool caught = false;
        try {
            py::module_::import("widget_module");
        } catch (pybind11::error_already_set &pe) {
            T_REQUIRE(pe.matches(PyExc_ImportError));
            std::string msg(pe.what());
            T_REQUIRE(msg.find("does not support loading in subinterpreters")
                      != std::string::npos);
            caught = true;
        }
        T_REQUIRE(caught);

Though it's clearly printing out:

Subinterpreter
-------------------------------------------------------------------------------
/home/runner/work/pybind11/pybind11/tests/test_embed/test_interpreter.cpp:339
...............................................................................

/home/runner/work/pybind11/pybind11/tests/test_embed/test_interpreter.cpp:339: FAILED:
  {Unknown expression after the reported line}
due to unexpected exception with message:
  ImportError: module widget_module does not support loading in subinterpreters

-------------------------------------------------------------------------------
Per-Subinterpreter GIL
-------------------------------------------------------------------------------
/home/runner/work/pybind11/pybind11/tests/test_embed/test_interpreter.cpp:470
...............................................................................

Is something different about catching exceptions in the free-threading build?

macOS (both) and ubuntu 3.13t fail. ubuntu 3.14t and Windows (both) hang.

@b-pass
Copy link
Contributor

b-pass commented May 16, 2025

In the free threading build for 3.13t and 3.14t, this isn't working:

[...]

Is something different about catching exceptions in the free-threading build?

macOS (both) and ubuntu 3.13t fail. ubuntu 3.14t and Windows (both) hang.

I think the issue is concurrent calls to the module init for the embedded modules. I think the below diff would fix it. I can make a separate PR for this if you want. I am working on another PR that includes for switching PYBIND11_EMBEDDED_MODULE to multiphase init (so that it can support py::mod_gil_not_used() and py::multiple_interpreters), that might be ready to start review soon....

I don't have any easy way to test this in 3.14t locally, sorry

diff --git a/include/pybind11/embed.h b/include/pybind11/embed.h
index a456e80a..b7bd3bec 100644
--- a/include/pybind11/embed.h
+++ b/include/pybind11/embed.h
@@ -41,15 +41,18 @@
 #define PYBIND11_EMBEDDED_MODULE(name, variable)                                                  \
     static ::pybind11::module_::module_def PYBIND11_CONCAT(pybind11_module_def_, name);           \
     static void PYBIND11_CONCAT(pybind11_init_, name)(::pybind11::module_ &);                     \
-    static PyObject PYBIND11_CONCAT(*pybind11_init_wrapper_, name)() {                            \
-        auto m = ::pybind11::module_::create_extension_module(                                    \
-            PYBIND11_TOSTRING(name), nullptr, &PYBIND11_CONCAT(pybind11_module_def_, name));      \
-        try {                                                                                     \
-            PYBIND11_CONCAT(pybind11_init_, name)(m);                                             \
-            return m.ptr();                                                                       \
-        }                                                                                         \
-        PYBIND11_CATCH_INIT_EXCEPTIONS                                                            \
-        return nullptr;                                                                           \
+    static PyObject *PYBIND11_CONCAT(pybind11_init_wrapper_, name)() {                            \
+        static auto result = []() -> PyObject * {                                                 \
+            auto m = ::pybind11::module_::create_extension_module(                                \
+                PYBIND11_TOSTRING(name), nullptr, &PYBIND11_CONCAT(pybind11_module_def_, name));  \
+            try {                                                                                 \
+                PYBIND11_CONCAT(pybind11_init_, name)(m);                                         \
+                return m.ptr();                                                                   \
+            }                                                                                     \
+            PYBIND11_CATCH_INIT_EXCEPTIONS                                                        \
+            return nullptr;                                                                       \
+        }();                                                                                      \
+        return result;                                                                            \
     }                                                                                             \
     PYBIND11_EMBEDDED_MODULE_IMPL(name)                                                           \
     ::pybind11::detail::embedded_module PYBIND11_CONCAT(pybind11_module_, name)(                  \

Signed-off-by: Henry Schreiner <[email protected]>
@henryiii
Copy link
Collaborator Author

henryiii commented May 16, 2025

I'm low on battery, so I'll drop the free threaded ci and that can be a separate PR. I can try things locally normally but that will finish my battery. ;) I tried your patch but it looks like it's failing.

Sounds good.

henryiii added 2 commits May 16, 2025 18:23
Signed-off-by: Henry Schreiner <[email protected]>
@henryiii henryiii marked this pull request as ready for review May 16, 2025 23:41
@@ -86,7 +86,7 @@ def test_dependent_subinterpreters():

sys.path.append(".")

if sys.version_info >= (3, 14):
if sys.version_info >= (3, 15):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this final for 3.14, or could this still change before the 3.14.0 release?

If the latter: maybe add a comment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will continue to work. I think intepreters is supposed to be released on PyPI during the 3.14 lifecycle.

@henryiii henryiii merged commit 094343c into pybind:master May 17, 2025
66 of 67 checks passed
@henryiii henryiii deleted the henryiii/chore/py314 branch May 17, 2025 01:58
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label May 17, 2025
@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label May 17, 2025
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.

5 participants