From 91d71dd67074d4599b6bd49cc933f41f8bd57058 Mon Sep 17 00:00:00 2001 From: dzaramelcone <134235821+dzaramelcone@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:22:24 -0400 Subject: [PATCH 1/3] gh-149816: #96 Fix a race condition in invoke_gc_callback with free threading (GH-150029) --- Lib/test/test_free_threading/test_gc.py | 21 ++++++++++ ...-05-18-12-32-33.gh-issue-149816.v18Ypf.rst | 2 + Python/gc_free_threading.c | 38 ++++++++++--------- 3 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst diff --git a/Lib/test/test_free_threading/test_gc.py b/Lib/test/test_free_threading/test_gc.py index 399010234509408..d1522a1d6da14e0 100644 --- a/Lib/test/test_free_threading/test_gc.py +++ b/Lib/test/test_free_threading/test_gc.py @@ -2,6 +2,7 @@ import threading from threading import Thread +import time from unittest import TestCase import gc @@ -94,6 +95,26 @@ def evil(): thread.start() thread.join() + def test_gc_callbacks_race_with_mutation(self): + def collect(): + b.wait() + while not stop.is_set(): + gc.collect() + + def mutate(): + b.wait() + while not stop.is_set(): + gc.callbacks[:] = [lambda *_: _ for _ in range(16)] + time.sleep(0) + gc.callbacks.clear() + + threads = [threading.Thread(target=f) for f in (collect, mutate) * 4] + b = threading.Barrier(len(threads) + 1) + stop = threading.Event() + + with threading_helper.start_threads(threads, stop.set): + b.wait() + time.sleep(0.2) def test_set_threshold(self): # GH-148613: Setting the GC threshold from another thread could cause a # race between the `gc_should_collect` and `gc_set_threshold` functions. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst new file mode 100644 index 000000000000000..bf7e4a624250e3e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst @@ -0,0 +1,2 @@ +Fix race conditions in ``invoke_gc_callback`` iterating ``gc.callbacks`` +in free-threading mode. diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 99f1a1eb47e3ddc..fbd13d1e4d87f25 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -9,6 +9,7 @@ #include "pycore_initconfig.h" // _PyStatus_NO_MEMORY() #include "pycore_interp.h" // PyInterpreterState.gc #include "pycore_interpframe.h" // _PyFrame_GetLocalsArray() +#include "pycore_list.h" // _PyList_GetItemRef() #include "pycore_object_alloc.h" // _PyObject_MallocWithType() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_tstate.h" // _PyThreadStateImpl @@ -1940,24 +1941,25 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase, /* The local variable cannot be rebound, check it for sanity */ assert(PyList_CheckExact(gcstate->callbacks)); - PyObject *info = NULL; - if (PyList_GET_SIZE(gcstate->callbacks) != 0) { - info = Py_BuildValue("{sisnsnsnsd}", - "generation", generation, - "collected", collected, - "uncollectable", uncollectable, - "candidates", candidates, - "duration", duration); - if (info == NULL) { - PyErr_FormatUnraisable("Exception ignored while " - "invoking gc callbacks"); - return; - } + if (PyList_GET_SIZE(gcstate->callbacks) == 0) { + return; + } + + PyObject *info = Py_BuildValue("{sisnsnsnsd}", + "generation", generation, + "collected", collected, + "uncollectable", uncollectable, + "candidates", candidates, + "duration", duration); + if (info == NULL) { + PyErr_FormatUnraisable("Exception ignored while " + "invoking gc callbacks"); + return; } PyObject *phase_obj = PyUnicode_FromString(phase); if (phase_obj == NULL) { - Py_XDECREF(info); + Py_DECREF(info); PyErr_FormatUnraisable("Exception ignored while " "invoking gc callbacks"); return; @@ -1965,8 +1967,10 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase, PyObject *stack[] = {phase_obj, info}; for (Py_ssize_t i=0; icallbacks); i++) { - PyObject *r, *cb = PyList_GET_ITEM(gcstate->callbacks, i); - Py_INCREF(cb); /* make sure cb doesn't go away */ + PyObject *r, *cb = _PyList_GetItemRef((PyListObject *)gcstate->callbacks, i); + if (cb == NULL) { + break; + } r = PyObject_Vectorcall(cb, stack, 2, NULL); if (r == NULL) { PyErr_FormatUnraisable("Exception ignored while " @@ -1978,7 +1982,7 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase, Py_DECREF(cb); } Py_DECREF(phase_obj); - Py_XDECREF(info); + Py_DECREF(info); assert(!_PyErr_Occurred(tstate)); } From 228b1bf745836e5f2097ee273d2c3f731fd1c3d5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 20 Aug 2026 17:02:56 +0200 Subject: [PATCH 2/3] gh-65961: Document __cached__ removal in What's New & clarify references to it (GH-156053) - Mention the change in What's New - Clarify references to ``__cached__`` now that the term it doesn't link to its docs. - Move the ``.. versionchanged`` from ``__file__`` docs to the top level. --- Doc/c-api/import.rst | 4 ++-- Doc/library/runpy.rst | 4 ++-- Doc/reference/datamodel.rst | 11 +++-------- Doc/whatsnew/3.15.rst | 8 ++++++++ 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Doc/c-api/import.rst b/Doc/c-api/import.rst index b48cf951137e511..7a3e5357e0c2cac 100644 --- a/Doc/c-api/import.rst +++ b/Doc/c-api/import.rst @@ -146,7 +146,7 @@ Importing Modules alternatives. .. versionchanged:: 3.15 - ``__cached__`` is no longer set. + The ``__cached__`` attribute is no longer set. .. c:function:: PyObject* PyImport_ExecCodeModuleEx(const char *name, PyObject *co, const char *pathname) @@ -170,7 +170,7 @@ Importing Modules :class:`~importlib.machinery.ModuleSpec` for alternatives. .. versionchanged:: 3.15 - ``__cached__`` no longer set. + The ``__cached__`` attribute no longer set. .. c:function:: PyObject* PyImport_ExecCodeModuleWithPathnames(const char *name, PyObject *co, const char *pathname, const char *cpathname) diff --git a/Doc/library/runpy.rst b/Doc/library/runpy.rst index d764a98c8419ed3..9e1ec4691872f05 100644 --- a/Doc/library/runpy.rst +++ b/Doc/library/runpy.rst @@ -97,7 +97,7 @@ The :mod:`!runpy` module provides two functions: :class:`~importlib.machinery.ModuleSpec` for alternatives. .. versionchanged:: 3.15 - ``__cached__`` is no longer set. + The global variable ``__cached__`` is no longer set. .. function:: run_path(path_name, init_globals=None, run_name=None) @@ -175,7 +175,7 @@ The :mod:`!runpy` module provides two functions: ``__package__`` are deprecated. .. versionchanged:: 3.15 - ``__cached__`` is no longer set. + The global variable ``__cached__`` is no longer set. .. seealso:: diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 2a961a062780f46..fde3cef63bc6e90 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1099,14 +1099,9 @@ this approach. :ref:`import system ` may opt to leave it unset if it has no semantic meaning (for example, a module loaded from a database). - .. deprecated-removed:: 3.13 3.15 - Setting ``__cached__`` on a module while failing to set - :attr:`!__spec__.cached` is deprecated. In Python 3.15, - ``__cached__`` will cease to be set or taken into consideration by - the import system or standard library. - - .. versionchanged:: 3.15 - ``__cached__`` is no longer set. +.. versionchanged:: 3.15 + The ``__cached__`` attribute is no longer set on modules or taken into + consideration by the import system or standard library. Other writable attributes on module objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5a8ab88a30fcf58..9a1efd811eb2bc7 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -880,6 +880,14 @@ Other language changes other, as regular dynamic extensions do. (Contributed by Stefano Rivera in :gh:`122931`.) +* The ``__cached__`` attribute on modules, which was deprecated since version + 3.13, is no longer set or taken into consideration by the import system or + standard library. + Use :attr:`__spec__.cached ` instead. + (Contributed by Brett Cannon in :gh:`97879`) + + Note that the :attr:`~module.__loader__` and :attr:`~module.__package__` + attributes are also deprecated and scheduled for removal. Default interactive shell From 9721f8f03510685c9209ce382fdbce3d2c5f8110 Mon Sep 17 00:00:00 2001 From: Aniket <148300120+Aniketsy@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:22:18 +0530 Subject: [PATCH 3/3] gh-128326: Normalize self-references to "documentation" (#143138) Co-authored-by: Stan Ulbrych --- Doc/about.rst | 2 +- Doc/tools/templates/indexsidebar.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/about.rst b/Doc/about.rst index 5c1b497ca6bcea8..84ae3492804c328 100644 --- a/Doc/about.rst +++ b/Doc/about.rst @@ -10,7 +10,7 @@ and now maintained as an independent project. .. _reStructuredText: https://docutils.sourceforge.io/rst.html .. _Sphinx: https://www.sphinx-doc.org/ -.. In the online version of these documents, you can submit comments and suggest +.. In the online version of this documentation, you can submit comments and suggest changes directly on the documentation pages. Development of the documentation and its toolchain is an entirely volunteer diff --git a/Doc/tools/templates/indexsidebar.html b/Doc/tools/templates/indexsidebar.html index 5e7f03cd024ef7f..3fd707babd782bb 100644 --- a/Doc/tools/templates/indexsidebar.html +++ b/Doc/tools/templates/indexsidebar.html @@ -1,5 +1,5 @@

{% trans %}Download{% endtrans %}

-

{% trans %}Download these documents{% endtrans %}

+

{% trans %}Download the documentation{% endtrans %}

{% trans %}Docs by version{% endtrans %}

    {# _docs_by_version.html is overwritten by build_docs.py for non-EOL versions #}