From b527c010bf3b77e0f77c7779f931989f5228ae98 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Fri, 31 Jul 2026 11:28:03 -0400 Subject: [PATCH 1/5] gh-75537: Add gc.ensure_disabled() context manager Add a context manager to the gc module that temporarily disables the cyclic garbage collector and reliably restores it to its previous state on exit. Uses PyGC_Disable() which returns the old state, making nesting and restoration atomic under the GIL. The feature was originally merged in 2018 (PR #4224) but reverted due to edge cases around GIL release, debug build crashes, and unclear nesting semantics. The modern C API (PyGC_Disable returning old state) resolves the state-tracking issues that caused the revert. --- Lib/test/test_gc.py | 32 ++++++++++++++++++ Modules/gcmodule.c | 82 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 4c721c01a34f070..b52664ad32f46d0 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1680,6 +1680,38 @@ def test_threshold_zero(self): self.assertEqual(i, 50001) + def test_ensure_disabled(self): + # gc.ensure_disabled() context manager + self.assertTrue(gc.isenabled()) + with gc.ensure_disabled(): + self.assertFalse(gc.isenabled()) + self.assertTrue(gc.isenabled()) + + def test_ensure_disabled_nesting(self): + self.assertTrue(gc.isenabled()) + with gc.ensure_disabled(): + self.assertFalse(gc.isenabled()) + with gc.ensure_disabled(): + self.assertFalse(gc.isenabled()) + self.assertFalse(gc.isenabled()) + self.assertTrue(gc.isenabled()) + + def test_ensure_disabled_already_disabled(self): + gc.disable() + self.assertFalse(gc.isenabled()) + with gc.ensure_disabled(): + self.assertFalse(gc.isenabled()) + self.assertFalse(gc.isenabled()) + gc.enable() + + def test_ensure_disabled_exception(self): + self.assertTrue(gc.isenabled()) + with self.assertRaises(ValueError): + with gc.ensure_disabled(): + self.assertFalse(gc.isenabled()) + raise ValueError("test") + self.assertTrue(gc.isenabled()) + class PythonFinalizationTests(unittest.TestCase): def test_ast_fini(self): diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index e2df31556f3c372..11cad03286976e6 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -65,6 +65,80 @@ gc_isenabled_impl(PyObject *module) return PyGC_IsEnabled(); } + +/* Context manager to temporarily disable the garbage collector. */ + +typedef struct { + PyObject_HEAD + int old_state; +} _gc_ensure_disabled_state; + +static void +_gc_ensure_disabled_dealloc(PyObject *self) +{ + PyObject_Free(self); +} + +static PyObject * +_gc_ensure_disabled_enter(PyObject *self, PyObject *Py_UNUSED(args)) +{ + Py_RETURN_NONE; +} + +static PyObject * +_gc_ensure_disabled_exit(PyObject *self, PyObject *args) +{ + _gc_ensure_disabled_state *s = (_gc_ensure_disabled_state *)self; + if (s->old_state) { + PyGC_Enable(); + } + Py_RETURN_NONE; + if (s->old_state) { + PyGC_Enable(); + } + Py_RETURN_NONE; +} + +static PyMethodDef _gc_ensure_disabled_methods[] = { + {"__enter__", _gc_ensure_disabled_enter, METH_NOARGS, NULL}, + {"__exit__", _gc_ensure_disabled_exit, METH_VARARGS, NULL}, + {NULL, NULL, 0, NULL} +}; + +static PyTypeObject _GCEnsureDisabled_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "gc._ensure_disabled", + .tp_basicsize = sizeof(_gc_ensure_disabled_state), + .tp_dealloc = _gc_ensure_disabled_dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = _gc_ensure_disabled_methods, +}; + + +PyDoc_STRVAR(gc_ensure_disabled__doc__, +"ensure_disabled() -> context manager\n" +"\n" +"Context manager to temporarily disable the garbage collector.\n" +"\n" +"At the start of the block the garbage collector is disabled.\n" +"On exit, it is restored to its previous state.\n" +"\n" +"Example:\n" +" with gc.ensure_disabled():\n" +" ... # GC is disabled during this block\n"); + +static PyObject * +gc_ensure_disabled(PyObject *module, PyObject *Py_UNUSED(args)) +{ + _gc_ensure_disabled_state *ctx = PyObject_New( + _gc_ensure_disabled_state, &_GCEnsureDisabled_Type); + if (ctx == NULL) { + return NULL; + } + ctx->old_state = PyGC_Disable(); + return (PyObject *)ctx; +} + /*[clinic input] gc.collect -> Py_ssize_t @@ -521,7 +595,8 @@ PyDoc_STRVAR(gc__doc__, "get_referents() -- Return the list of objects that an object refers to.\n" "freeze() -- Freeze all tracked objects and ignore them for future collections.\n" "unfreeze() -- Unfreeze all objects in the permanent generation.\n" -"get_freeze_count() -- Return the number of objects in the permanent generation.\n"); +"get_freeze_count() -- Return the number of objects in the permanent generation.\n" +"ensure_disabled() -- Context manager to temporarily disable the garbage collector.\n"); static PyMethodDef GcMethods[] = { GC_ENABLE_METHODDEF @@ -542,6 +617,7 @@ static PyMethodDef GcMethods[] = { GC_FREEZE_METHODDEF GC_UNFREEZE_METHODDEF GC_GET_FREEZE_COUNT_METHODDEF + {"ensure_disabled", gc_ensure_disabled, METH_NOARGS, gc_ensure_disabled__doc__}, {NULL, NULL} /* Sentinel */ }; @@ -550,6 +626,10 @@ gcmodule_exec(PyObject *module) { GCState *gcstate = get_gc_state(); + if (PyType_Ready(&_GCEnsureDisabled_Type) < 0) { + return -1; + } + /* garbage and callbacks are initialized by _PyGC_Init() early in * interpreter lifecycle. */ assert(gcstate->garbage != NULL); From 83d9729482ecdc6a0eb5325fe36f9c06daf3f480 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Fri, 31 Jul 2026 11:28:45 -0400 Subject: [PATCH 2/5] Add doc and NEWS for gc.ensure_disabled() --- Doc/library/gc.rst | 16 ++++++++++++++++ Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst diff --git a/Doc/library/gc.rst b/Doc/library/gc.rst index 65533e7e57adc33..90cc3ff23483b6a 100644 --- a/Doc/library/gc.rst +++ b/Doc/library/gc.rst @@ -35,6 +35,22 @@ The :mod:`!gc` module provides the following functions: Return ``True`` if automatic collection is enabled. +.. function:: ensure_disabled() + + Return a context manager that temporarily disables the garbage + collector. The collector is disabled at the start of the ``with`` + block and restored to its previous state on exit:: + + with gc.ensure_disabled(): + ... # GC is disabled during this block + + Nesting is supported — each level saves and restores its own state. + If the collector was already disabled before entering the block, it + remains disabled after exit. + + .. versionadded:: 3.16 + + .. function:: collect(generation=2) With no arguments, run a full collection. The optional argument *generation* diff --git a/Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst b/Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst new file mode 100644 index 000000000000000..e50865adf621cb6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst @@ -0,0 +1,2 @@ +Add :func:`gc.ensure_disabled` context manager to temporarily disable +the garbage collector. By Saksham Kapoor in :issue:`75537`. From cab16ea1ad315111773bca64c49d066b4fe43bfc Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Fri, 31 Jul 2026 11:33:06 -0400 Subject: [PATCH 3/5] Fix NEWS entry filename for blurb format --- ...gh-75537.rst => 2026-07-31-15-35-00.gh-issue-75537.852b0b.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Library/{2026-07-31-gh-75537.rst => 2026-07-31-15-35-00.gh-issue-75537.852b0b.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst b/Misc/NEWS.d/next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2026-07-31-gh-75537.rst rename to Misc/NEWS.d/next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst From 60631ff2631391b9438db5af3b74d474fe2f3087 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Fri, 31 Jul 2026 11:39:59 -0400 Subject: [PATCH 4/5] Remove duplicate enable in __exit__ (dead code after Py_RETURN_NONE) --- Modules/gcmodule.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 11cad03286976e6..6508e476acbe999 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -93,10 +93,6 @@ _gc_ensure_disabled_exit(PyObject *self, PyObject *args) PyGC_Enable(); } Py_RETURN_NONE; - if (s->old_state) { - PyGC_Enable(); - } - Py_RETURN_NONE; } static PyMethodDef _gc_ensure_disabled_methods[] = { From 5a55da353864099747515712a68abbcf833b3c9a Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Fri, 31 Jul 2026 11:49:06 -0400 Subject: [PATCH 5/5] Remove issue reference from NEWS entry (blurb handles it from filename) --- .../next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst | 2 +- Tools/c-analyzer/cpython/globals-to-fix.tsv | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst b/Misc/NEWS.d/next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst index e50865adf621cb6..924744c8d3c060d 100644 --- a/Misc/NEWS.d/next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst +++ b/Misc/NEWS.d/next/Library/2026-07-31-15-35-00.gh-issue-75537.852b0b.rst @@ -1,2 +1,2 @@ Add :func:`gc.ensure_disabled` context manager to temporarily disable -the garbage collector. By Saksham Kapoor in :issue:`75537`. +the garbage collector. diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index db575d870be5c53..0d1bb2acc1bc3ab 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -454,3 +454,4 @@ Modules/rotatingtree.c - random_value - Modules/rotatingtree.c - random_mutex - Modules/socketmodule.c - accept4_works - Modules/socketmodule.c - sock_cloexec_works - +Modules/gcmodule.c - _GCEnsureDisabled_Type -