https://github.com/python/cpython/commit/6920036f287480f7d39d6a4005803aeac27aff3f
commit: 6920036f287480f7d39d6a4005803aeac27aff3f
branch: main
author: Donghee Na <[email protected]>
committer: corona10 <[email protected]>
date: 2026-06-22T23:45:55Z
summary:
gh-151722: Defer GC tracking of frozendict to end of construction (gh-151740)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst
M Objects/dictobject.c
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst
new file mode 100644
index 00000000000000..57b5dee7458ede
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-20-00-30-47.gh-issue-151722.RPMPIY.rst
@@ -0,0 +1,2 @@
+Defer GC tracking of :class:`frozendict` to end of construction. Patch by
+Donghee Na.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index ac2f210d023487..9210398ee551de 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -138,6 +138,7 @@ As a consequence of this, split keys have a maximum size of
16.
// Forward declarations
static PyObject* frozendict_new(PyTypeObject *type, PyObject *args,
PyObject *kwds);
+static PyObject* frozendict_new_untracked(PyTypeObject *type);
static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
static int dict_merge(PyObject *a, PyObject *b, int override, PyObject
**dupkey);
static int dict_contains(PyObject *op, PyObject *key);
@@ -4146,12 +4147,9 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other,
int override, PyObject **
set_keys(mp, keys);
STORE_USED(mp, other->ma_used);
ASSERT_CONSISTENT(mp);
-
- if (_PyObject_GC_IS_TRACKED(other) &&
!_PyObject_GC_IS_TRACKED(mp)) {
- /* Maintain tracking. */
- _PyObject_GC_TRACK(mp);
+ if (PyDict_Check(mp)) {
+ assert(_PyObject_GC_IS_TRACKED(mp));
}
-
return 0;
}
}
@@ -5242,14 +5240,13 @@ static PyNumberMethods dict_as_number = {
};
static PyObject *
-dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+dict_new_untracked(PyTypeObject *type)
{
assert(type != NULL);
- assert(type->tp_alloc != NULL);
// dict subclasses must implement the GC protocol
assert(_PyType_IS_GC(type));
- PyObject *self = type->tp_alloc(type, 0);
+ PyObject *self = _PyType_AllocNoTrack(type, 0);
if (self == NULL) {
return NULL;
}
@@ -5262,9 +5259,19 @@ dict_new(PyTypeObject *type, PyObject *args, PyObject
*kwds)
d->ma_keys = Py_EMPTY_KEYS;
d->ma_values = NULL;
ASSERT_CONSISTENT(d);
- if (!_PyObject_GC_IS_TRACKED(d)) {
- _PyObject_GC_TRACK(d);
+ return self;
+}
+
+static PyObject *
+dict_new(PyTypeObject *type, PyObject *Py_UNUSED(args), PyObject
*Py_UNUSED(kwds))
+{
+ /* tp_new ignores args/kwds; args/kwds are consumed by dict_init
(tp_init). */
+ PyObject *self = dict_new_untracked(type);
+ if (self == NULL) {
+ return NULL;
}
+ assert(!_PyObject_GC_IS_TRACKED(self));
+ _PyObject_GC_TRACK(self);
return self;
}
@@ -5323,7 +5330,9 @@ frozendict_vectorcall(PyObject *type, PyObject *
const*args,
return Py_NewRef(args[0]);
}
- PyObject *self = frozendict_new(_PyType_CAST(type), NULL, NULL);
+ /* gh-151722: Keep the frozendict untracked until it is fully built,
+ so a half-built object is never reachable from another thread (using
the gc module). */
+ PyObject *self = frozendict_new_untracked(_PyType_CAST(type));
if (self == NULL) {
return NULL;
}
@@ -5343,6 +5352,8 @@ frozendict_vectorcall(PyObject *type, PyObject *
const*args,
}
}
}
+ assert(!_PyObject_GC_IS_TRACKED(self));
+ _PyObject_GC_TRACK(self);
return self;
}
@@ -8361,17 +8372,27 @@ frozendict_hash(PyObject *op)
}
+/* Allocate an empty, GC-untracked frozendict; the constructor tracks it once
+ fully built. */
static PyObject *
-frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+frozendict_new_untracked(PyTypeObject *type)
{
- PyObject *d = dict_new(type, args, kwds);
+ PyObject *d = dict_new_untracked(type);
if (d == NULL) {
return NULL;
}
assert(can_modify_dict(_PyAnyDict_CAST(d)));
+ _PyFrozenDictObject_CAST(d)->ma_hash = -1;
+ return d;
+}
- PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d);
- self->ma_hash = -1;
+static PyObject *
+frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ PyObject *d = frozendict_new_untracked(type);
+ if (d == NULL) {
+ return NULL;
+ }
if (args != NULL) {
if (dict_update_common(d, args, kwds, "frozendict") < 0) {
@@ -8383,6 +8404,8 @@ frozendict_new(PyTypeObject *type, PyObject *args,
PyObject *kwds)
assert(kwds == NULL);
}
+ assert(!_PyObject_GC_IS_TRACKED(d));
+ _PyObject_GC_TRACK(d);
return d;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]