From 09281e0910d5396e6486dfa43b84a6d6bde2c465 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 12 Apr 2026 21:12:23 +0000 Subject: [PATCH 01/13] restore historical behavior of `defaultdict` Partial revert of GH-142668 (a0434075108efe6acdfba34f42545f4d80ac9a5e). --- Lib/test/test_defaultdict.py | 10 +++++++--- Modules/_collectionsmodule.c | 10 +++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_defaultdict.py b/Lib/test/test_defaultdict.py index a193eb10f16d17..5d3cc3bf72983c 100644 --- a/Lib/test/test_defaultdict.py +++ b/Lib/test/test_defaultdict.py @@ -51,7 +51,11 @@ def test_missing(self): d1 = defaultdict() self.assertRaises(KeyError, d1.__missing__, 42) d1.default_factory = list - self.assertEqual(d1.__missing__(42), []) + v1 = d1.__missing__(42) + self.assertEqual(v1, []) + v2 = d1.__missing__(42) + self.assertEqual(v2, []) + self.assertIsNot(v2, v1) def test_repr(self): d1 = defaultdict() @@ -186,7 +190,7 @@ def test_union(self): with self.assertRaises(TypeError): i |= None - def test_factory_conflict_with_set_value(self): + def test_reentering_getitem_method(self): key = "conflict_test" count = 0 @@ -201,7 +205,7 @@ def default_factory(): test_dict = defaultdict(default_factory) self.assertEqual(count, 0) - self.assertEqual(test_dict[key], 2) + self.assertEqual(test_dict[key], 1) self.assertEqual(count, 2) def test_repr_recursive_factory(self): diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 4ff05727ebc8ce..93bf4beeb0c20b 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2247,11 +2247,11 @@ defdict_missing(PyObject *op, PyObject *key) value = _PyObject_CallNoArgs(factory); if (value == NULL) return value; - PyObject *result = NULL; - (void)PyDict_SetDefaultRef(op, key, value, &result); - // 'result' is NULL, or a strong reference to 'value' or 'op[key]' - Py_DECREF(value); - return result; + if (PyObject_SetItem(op, key, value) < 0) { + Py_DECREF(value); + return NULL; + } + return value; } static inline PyObject* From b1a9ae217a4a0fad25cdde001ba8103a8c5e240a Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 12 Apr 2026 21:22:35 +0000 Subject: [PATCH 02/13] simplify the `defdict_missing` function --- Modules/_collectionsmodule.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 93bf4beeb0c20b..90a1a586ecd6d7 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2237,11 +2237,7 @@ defdict_missing(PyObject *op, PyObject *key) PyObject *value; if (factory == NULL || factory == Py_None) { /* XXX Call dict.__missing__(key) */ - PyObject *tup; - tup = PyTuple_Pack(1, key); - if (!tup) return NULL; - PyErr_SetObject(PyExc_KeyError, tup); - Py_DECREF(tup); + _PyErr_SetKeyError(key); return NULL; } value = _PyObject_CallNoArgs(factory); From 25027cd96d2af9f9b7562759088b1575570e1ce5 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 12 Apr 2026 21:22:55 +0000 Subject: [PATCH 03/13] remove bad comment in `defdict_missing` --- Modules/_collectionsmodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 90a1a586ecd6d7..90eecdf175542e 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2236,7 +2236,6 @@ defdict_missing(PyObject *op, PyObject *key) PyObject *factory = dd->default_factory; PyObject *value; if (factory == NULL || factory == Py_None) { - /* XXX Call dict.__missing__(key) */ _PyErr_SetKeyError(key); return NULL; } From 81675cf1073e4f33dd2825b86f629641babf6bc3 Mon Sep 17 00:00:00 2001 From: Changaco Date: Sun, 12 Apr 2026 22:44:33 +0000 Subject: [PATCH 04/13] create thread-safe `defaultdict.__getitem__` method --- Doc/library/collections.rst | 34 ++++++++++++++++++++----------- Include/internal/pycore_dict.h | 2 ++ Modules/_collectionsmodule.c | 37 ++++++++++++++++++++++++++++++++-- Objects/dictobject.c | 28 ++++++++++++------------- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index cb9300f072b9e7..4930d83ae0692d 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -730,7 +730,7 @@ stack manipulations such as ``dup``, ``drop``, ``swap``, ``over``, ``pick``, defaultdict(default_factory, iterable, /, **kwargs) Return a new dictionary-like object. :class:`defaultdict` is a subclass of the - built-in :class:`dict` class. It overrides one method and adds one writable + built-in :class:`dict` class. It defines two methods and adds one writable instance variable. The remaining functionality is the same as for the :class:`dict` class and is not documented here. @@ -740,8 +740,15 @@ stack manipulations such as ``dup``, ``drop``, ``swap``, ``over``, ``pick``, arguments. - :class:`defaultdict` objects support the following method in addition to the - standard :class:`dict` operations: + :class:`defaultdict` defines the following methods: + + .. method:: __getitem__(key, /) + + Does exactly the same thing as :meth:`dict.__getitem__`, but in a more + :term:`thread-safe` way. When :term:`free threading` is enabled, the + defaultdict is locked while the key is being looked up and the + :meth:`__missing__` method is being called, thus ensuring that only one + default value is generated and inserted for each missing key. .. method:: __missing__(key, /) @@ -755,28 +762,31 @@ stack manipulations such as ``dup``, ``drop``, ``swap``, ``over``, ``pick``, If calling :attr:`default_factory` raises an exception this exception is propagated unchanged. - This method is called by the :meth:`~object.__getitem__` method of the - :class:`dict` class when the requested key is not found; whatever it - returns or raises is then returned or raised by :meth:`~object.__getitem__`. + This method is called by the :meth:`__getitem__` method when the requested + key is not found; whatever it returns or raises is then returned or raised + by :meth:`__getitem__`. Note that :meth:`__missing__` is *not* called for any operations besides - :meth:`~object.__getitem__`. This means that :meth:`~dict.get` will, like - normal dictionaries, return ``None`` as a default rather than using - :attr:`default_factory`. + `self[key]`. This means that `self.get(key)` will, like normal dictionaries, + return ``None`` as a default rather than using :attr:`default_factory`. :class:`defaultdict` objects support the following instance variable: - .. attribute:: default_factory This attribute is used by the :meth:`~defaultdict.__missing__` method; it is initialized from the first argument to the constructor, if present, or to ``None``, if absent. + .. versionchanged:: 3.9 - Added merge (``|``) and update (``|=``) operators, specified in - :pep:`584`. + Added merge (``|``) and update (``|=``) operators, specified in + :pep:`584`. + + .. versionchanged:: 3.15 + Added the :meth:`__getitem__` method which is safe to use with + :term:`free threading` enabled. :class:`defaultdict` Examples diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 6c6e3b77e69fab..56a29c586f67f5 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -123,6 +123,8 @@ PyAPI_FUNC(Py_ssize_t) _Py_dict_lookup(PyDictObject *mp, PyObject *key, Py_hash_ extern Py_ssize_t _Py_dict_lookup_threadsafe(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject **value_addr); extern Py_ssize_t _Py_dict_lookup_threadsafe_stackref(PyDictObject *mp, PyObject *key, Py_hash_t hash, _PyStackRef *value_addr); +extern void _Py_dict_unhashable_type(PyObject *op, PyObject *key); + extern int _PyDict_GetMethodStackRef(PyDictObject *dict, PyObject *name, _PyStackRef *method); // Exported for external JIT support diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 90eecdf175542e..d668f11aec814b 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2222,9 +2222,39 @@ typedef struct { static PyType_Spec defdict_spec; +PyDoc_STRVAR(defdict_getitem_doc, +"__getitem__($self, key, /)\n--\n\n\ +Return self[key]. Try to create the item if it doesn't exist, by calling\n\ +self.__missing__(key).\ +"); + +static PyObject * +defdict_subscript(PyObject *op, PyObject *key) +{ + Py_ssize_t ix; + Py_hash_t hash; + PyObject *value; + + hash = _PyObject_HashFast(key); + if (hash == -1) { + _Py_dict_unhashable_type(op, key); + return NULL; + } + Py_BEGIN_CRITICAL_SECTION(op); + ix = _Py_dict_lookup((PyDictObject *)op, key, hash, &value); + if (value != NULL) { + Py_INCREF(value); + } else if (ix != DKIX_ERROR) { + value = PyObject_CallMethodOneArg(op, &_Py_ID(__missing__), key); + } + Py_END_CRITICAL_SECTION(); + return value; +} + PyDoc_STRVAR(defdict_missing_doc, -"__missing__(key) # Called by __getitem__ for missing key; pseudo-code:\n\ - if self.default_factory is None: raise KeyError((key,))\n\ +"__missing__($self, key, /)\n--\n\n\ + # Called by __getitem__ for missing key. Equivalent to:\n\ + if self.default_factory is None: raise KeyError(key)\n\ self[key] = value = self.default_factory()\n\ return value\n\ "); @@ -2326,6 +2356,8 @@ defdict_reduce(PyObject *op, PyObject *Py_UNUSED(dummy)) } static PyMethodDef defdict_methods[] = { + {"__getitem__", defdict_subscript, METH_O|METH_COEXIST, + defdict_getitem_doc}, {"__missing__", defdict_missing, METH_O, defdict_missing_doc}, {"copy", defdict_copy, METH_NOARGS, @@ -2506,6 +2538,7 @@ static PyType_Slot defdict_slots[] = { {Py_tp_init, defdict_init}, {Py_tp_alloc, PyType_GenericAlloc}, {Py_tp_free, PyObject_GC_Del}, + {Py_mp_subscript, defdict_subscript}, {0, NULL}, }; diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 09db93b2d31820..1ae4be4b234ac9 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2398,8 +2398,8 @@ PyDict_GetItem(PyObject *op, PyObject *key) "PyDict_GetItemRef() or PyDict_GetItemWithError()"); } -static void -dict_unhashable_type(PyObject *op, PyObject *key) +void +_Py_dict_unhashable_type(PyObject *op, PyObject *key) { PyObject *exc = PyErr_GetRaisedException(); assert(exc != NULL); @@ -2428,7 +2428,7 @@ _PyDict_LookupIndexAndValue(PyDictObject *mp, PyObject *key, PyObject **value) Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type((PyObject*)mp, key); + _Py_dict_unhashable_type((PyObject*)mp, key); return -1; } @@ -2532,7 +2532,7 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result) Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(op, key); + _Py_dict_unhashable_type(op, key); *result = NULL; return -1; } @@ -2548,7 +2548,7 @@ _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject ** Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type((PyObject*)op, key); + _Py_dict_unhashable_type((PyObject*)op, key); *result = NULL; return -1; } @@ -2586,7 +2586,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key) } hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(op, key); + _Py_dict_unhashable_type(op, key); return NULL; } @@ -2746,7 +2746,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) { Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type((PyObject*)mp, key); + _Py_dict_unhashable_type((PyObject*)mp, key); Py_DECREF(key); Py_DECREF(value); return -1; @@ -2924,7 +2924,7 @@ PyDict_DelItem(PyObject *op, PyObject *key) assert(key); Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(op, key); + _Py_dict_unhashable_type(op, key); return -1; } @@ -3266,7 +3266,7 @@ pop_lock_held(PyObject *op, PyObject *key, PyObject **result) Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(op, key); + _Py_dict_unhashable_type(op, key); if (result) { *result = NULL; } @@ -3679,7 +3679,7 @@ dict_subscript(PyObject *self, PyObject *key) hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(self, key); + _Py_dict_unhashable_type(self, key); return NULL; } ix = _Py_dict_lookup_threadsafe(mp, key, hash, &value); @@ -4650,7 +4650,7 @@ dict_get_impl(PyDictObject *self, PyObject *key, PyObject *default_value) hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type((PyObject*)self, key); + _Py_dict_unhashable_type((PyObject*)self, key); return NULL; } ix = _Py_dict_lookup_threadsafe(self, key, hash, &val); @@ -4687,7 +4687,7 @@ dict_setdefault_ref_lock_held(PyObject *d, PyObject *key, PyObject *default_valu hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(d, key); + _Py_dict_unhashable_type(d, key); if (result) { *result = NULL; } @@ -5128,7 +5128,7 @@ dict_contains(PyObject *op, PyObject *key) { Py_hash_t hash = _PyObject_HashFast(key); if (hash == -1) { - dict_unhashable_type(op, key); + _Py_dict_unhashable_type(op, key); return -1; } @@ -7234,7 +7234,7 @@ _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value) if (value == NULL) { Py_hash_t hash = _PyObject_HashFast(name); if (hash == -1) { - dict_unhashable_type((PyObject*)dict, name); + _Py_dict_unhashable_type((PyObject*)dict, name); return -1; } return _PyDict_DelItem_KnownHash_LockHeld((PyObject *)dict, name, hash); From a0a63f121a5be339307fcb339b8d7a352c8496f5 Mon Sep 17 00:00:00 2001 From: Changaco Date: Tue, 21 Apr 2026 12:32:45 +0000 Subject: [PATCH 05/13] add NEWS entry --- .../next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst b/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst new file mode 100644 index 00000000000000..7ac4113a058d71 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst @@ -0,0 +1,2 @@ +Restore the historical behavior of the :class:`defaultdict` class, while keeping +it safe to use with :term:`free threading`. From 127ba729b594fa6d8669ec7a4837ba653eb7201b Mon Sep 17 00:00:00 2001 From: Changaco Date: Tue, 21 Apr 2026 12:57:10 +0000 Subject: [PATCH 06/13] rewrite the documentation of `defaultdict.__missing__` --- Doc/library/collections.rst | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 4930d83ae0692d..e567c4fe846acf 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -752,23 +752,22 @@ stack manipulations such as ``dup``, ``drop``, ``swap``, ``over``, ``pick``, .. method:: __missing__(key, /) - If the :attr:`default_factory` attribute is ``None``, this raises a - :exc:`KeyError` exception with the *key* as argument. - - If :attr:`default_factory` is not ``None``, it is called without arguments - to provide a default value for the given *key*, this value is inserted in - the dictionary for the *key*, and returned. - - If calling :attr:`default_factory` raises an exception this exception is - propagated unchanged. - - This method is called by the :meth:`__getitem__` method when the requested - key is not found; whatever it returns or raises is then returned or raised - by :meth:`__getitem__`. - - Note that :meth:`__missing__` is *not* called for any operations besides - `self[key]`. This means that `self.get(key)` will, like normal dictionaries, - return ``None`` as a default rather than using :attr:`default_factory`. + Equivalent to:: + + if self.default_factory is None: + raise KeyError(key) + self[key] = value = self.default_factory() + return value + + Keep in mind that this method is *not* called for any operations besides + ``dd[key]``. This means that ``dd.get(key)`` will, like normal + dictionaries, return ``None`` as a default rather than using + :attr:`default_factory`. + + A direct call to this method (meaning a call that isn't coming from + :meth:`__getitem__`) can create a :term:`race condition`. To reset an + item to a default value the next time it's accessed, use the + :meth:`~dict.pop` method to safely remove the current value. :class:`defaultdict` objects support the following instance variable: From d89e73627c2405677a42a5a8b36b86c6a713c0a2 Mon Sep 17 00:00:00 2001 From: Changaco Date: Tue, 21 Apr 2026 22:17:13 +0000 Subject: [PATCH 07/13] emit thread safety warning in `defaultdict.__missing__` --- Include/internal/pycore_critical_section.h | 20 ++++++++++++++++++++ Modules/_collectionsmodule.c | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/Include/internal/pycore_critical_section.h b/Include/internal/pycore_critical_section.h index 2a2846b1296b90..b77ac6e7410346 100644 --- a/Include/internal/pycore_critical_section.h +++ b/Include/internal/pycore_critical_section.h @@ -252,6 +252,26 @@ _PyCriticalSection_AssertHeldObj(PyObject *op) #endif } +static inline void +_PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message) +{ +#ifdef Py_GIL_DISABLED + PyMutex *mutex = &_PyObject_CAST(op)->ob_mutex; + PyThreadState *tstate = _PyThreadState_GET(); + uintptr_t prev = tstate->critical_section; + if (prev & _Py_CRITICAL_SECTION_TWO_MUTEXES) { + PyCriticalSection2 *cs = (PyCriticalSection2 *)(prev & ~_Py_CRITICAL_SECTION_MASK); + if (cs == NULL || (cs->_cs_base._cs_mutex != mutex && cs->_cs_mutex2 != mutex)) + PyErr_WarnEx(NULL, message, 2); + } + else { + PyCriticalSection *cs = (PyCriticalSection *)(prev & ~_Py_CRITICAL_SECTION_MASK); + if (cs == NULL || cs->_cs_mutex != mutex) + PyErr_WarnEx(NULL, message, 2); + } +#endif +} + #undef Py_BEGIN_CRITICAL_SECTION # define Py_BEGIN_CRITICAL_SECTION(op) \ { \ diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index d668f11aec814b..efd4ad4997de2a 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2262,6 +2262,12 @@ PyDoc_STRVAR(defdict_missing_doc, static PyObject * defdict_missing(PyObject *op, PyObject *key) { + _PyCriticalSection_WarnIfNotHeld( + op, + "the defaultdict.__missing__ method should not be called directly; " + "use dd.pop(key, None) to safely trigger a reset to a default value " + "the next time key is accessed" + ); defdictobject *dd = defdictobject_CAST(op); PyObject *factory = dd->default_factory; PyObject *value; From d1c0424bb632b3f8548c887cce755b40d95b2e58 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 22 Apr 2026 17:15:36 +0000 Subject: [PATCH 08/13] add missing `#include` --- Modules/_collectionsmodule.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index efd4ad4997de2a..38a6a677808a80 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -1,5 +1,6 @@ #include "Python.h" #include "pycore_call.h" // _PyObject_CallNoArgs() +#include "pycore_critical_section.h" // _PyCriticalSection_WarnIfNotHeld() #include "pycore_dict.h" // _PyDict_GetItem_KnownHash() #include "pycore_long.h" // _PyLong_GetZero() #include "pycore_moduleobject.h" // _PyModule_GetState() From f9eb85f085cdbd343abcfd906d950e491ac8e391 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 22 Apr 2026 20:10:51 +0000 Subject: [PATCH 09/13] fix `_PyCriticalSection_WarnIfNotHeld` --- Include/internal/pycore_critical_section.h | 23 +++------------------- Python/critical_section.c | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Include/internal/pycore_critical_section.h b/Include/internal/pycore_critical_section.h index b77ac6e7410346..2fc0b21b79fca3 100644 --- a/Include/internal/pycore_critical_section.h +++ b/Include/internal/pycore_critical_section.h @@ -95,6 +95,9 @@ _PyCriticalSection2_BeginSlow(PyThreadState *tstate, PyCriticalSection2 *c, PyMu PyAPI_FUNC(void) _PyCriticalSection_SuspendAll(PyThreadState *tstate); +PyAPI_FUNC(void) +_PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message); + #ifdef Py_GIL_DISABLED static inline int @@ -252,26 +255,6 @@ _PyCriticalSection_AssertHeldObj(PyObject *op) #endif } -static inline void -_PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message) -{ -#ifdef Py_GIL_DISABLED - PyMutex *mutex = &_PyObject_CAST(op)->ob_mutex; - PyThreadState *tstate = _PyThreadState_GET(); - uintptr_t prev = tstate->critical_section; - if (prev & _Py_CRITICAL_SECTION_TWO_MUTEXES) { - PyCriticalSection2 *cs = (PyCriticalSection2 *)(prev & ~_Py_CRITICAL_SECTION_MASK); - if (cs == NULL || (cs->_cs_base._cs_mutex != mutex && cs->_cs_mutex2 != mutex)) - PyErr_WarnEx(NULL, message, 2); - } - else { - PyCriticalSection *cs = (PyCriticalSection *)(prev & ~_Py_CRITICAL_SECTION_MASK); - if (cs == NULL || cs->_cs_mutex != mutex) - PyErr_WarnEx(NULL, message, 2); - } -#endif -} - #undef Py_BEGIN_CRITICAL_SECTION # define Py_BEGIN_CRITICAL_SECTION(op) \ { \ diff --git a/Python/critical_section.c b/Python/critical_section.c index 98e23eda7cdd77..e37a498092ae8b 100644 --- a/Python/critical_section.c +++ b/Python/critical_section.c @@ -201,3 +201,23 @@ PyCriticalSection2_End(PyCriticalSection2 *c) _PyCriticalSection2_End(_PyThreadState_GET(), c); #endif } + +void +_PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message) +{ +#ifdef Py_GIL_DISABLED + PyMutex *mutex = &_PyObject_CAST(op)->ob_mutex; + PyThreadState *tstate = _PyThreadState_GET(); + uintptr_t prev = tstate->critical_section; + if (prev & _Py_CRITICAL_SECTION_TWO_MUTEXES) { + PyCriticalSection2 *cs = (PyCriticalSection2 *)(prev & ~_Py_CRITICAL_SECTION_MASK); + if (cs == NULL || (cs->_cs_base._cs_mutex != mutex && cs->_cs_mutex2 != mutex)) + PyErr_WarnEx(NULL, message, 2); + } + else { + PyCriticalSection *cs = (PyCriticalSection *)(prev & ~_Py_CRITICAL_SECTION_MASK); + if (cs == NULL || cs->_cs_mutex != mutex) + PyErr_WarnEx(NULL, message, 2); + } +#endif +} From c158b54b41d0c5230c48e5e158190f6e11e68ead Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 22 Apr 2026 20:14:23 +0000 Subject: [PATCH 10/13] fix broken ref in NEWS entry --- .../Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst b/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst index 7ac4113a058d71..377d206a59a7be 100644 --- a/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst +++ b/Misc/NEWS.d/next/Library/2026-04-21-12-06-41.gh-issue-148242.eCy0eS.rst @@ -1,2 +1,2 @@ -Restore the historical behavior of the :class:`defaultdict` class, while keeping -it safe to use with :term:`free threading`. +Restore the historical behavior of the :class:`~collections.defaultdict` class, +while keeping it safe to use with :term:`free threading`. From e455fc1108212155a93497cf73ad657f9f5a40c8 Mon Sep 17 00:00:00 2001 From: Changaco Date: Wed, 22 Apr 2026 20:26:38 +0000 Subject: [PATCH 11/13] fix broken ref and inconsistent writing style --- Doc/library/collections.rst | 12 +++++++----- Modules/_collectionsmodule.c | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index e567c4fe846acf..0b750bfca61be5 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -744,11 +744,13 @@ stack manipulations such as ``dup``, ``drop``, ``swap``, ``over``, ``pick``, .. method:: __getitem__(key, /) - Does exactly the same thing as :meth:`dict.__getitem__`, but in a more - :term:`thread-safe` way. When :term:`free threading` is enabled, the - defaultdict is locked while the key is being looked up and the - :meth:`__missing__` method is being called, thus ensuring that only one - default value is generated and inserted for each missing key. + Return ``self[key]``. If the item doesn't exist, the :meth:`__missing__` + method is called to create it. + + When :term:`free threading` is enabled, the defaultdict is locked while + the key is being looked up and the :meth:`__missing__` method is being + called, thus ensuring that only one default value is generated and + inserted for each missing key. .. method:: __missing__(key, /) diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 38a6a677808a80..759c81083eba4c 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2225,8 +2225,8 @@ static PyType_Spec defdict_spec; PyDoc_STRVAR(defdict_getitem_doc, "__getitem__($self, key, /)\n--\n\n\ -Return self[key]. Try to create the item if it doesn't exist, by calling\n\ -self.__missing__(key).\ +Return self[key]. If the item doesn't exist, self.__missing__(key) is called\n\ +to create it.\ "); static PyObject * From 808b4abdd14f6fc19c403f0ca8aad52d1ebb7abc Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 23 Apr 2026 19:10:03 +0000 Subject: [PATCH 12/13] fix `_PyCriticalSection_WarnIfNotHeld` further --- Include/internal/pycore_critical_section.h | 2 +- Modules/_collectionsmodule.c | 7 +++---- Python/critical_section.c | 7 ++++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Include/internal/pycore_critical_section.h b/Include/internal/pycore_critical_section.h index 2fc0b21b79fca3..415ef273223860 100644 --- a/Include/internal/pycore_critical_section.h +++ b/Include/internal/pycore_critical_section.h @@ -95,7 +95,7 @@ _PyCriticalSection2_BeginSlow(PyThreadState *tstate, PyCriticalSection2 *c, PyMu PyAPI_FUNC(void) _PyCriticalSection_SuspendAll(PyThreadState *tstate); -PyAPI_FUNC(void) +int _PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message); #ifdef Py_GIL_DISABLED diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index 759c81083eba4c..dbc4dc7571d13f 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2263,12 +2263,11 @@ PyDoc_STRVAR(defdict_missing_doc, static PyObject * defdict_missing(PyObject *op, PyObject *key) { - _PyCriticalSection_WarnIfNotHeld( - op, + if (_PyCriticalSection_WarnIfNotHeld(op, "the defaultdict.__missing__ method should not be called directly; " "use dd.pop(key, None) to safely trigger a reset to a default value " - "the next time key is accessed" - ); + "the next time key is accessed") < 0) + return NULL; defdictobject *dd = defdictobject_CAST(op); PyObject *factory = dd->default_factory; PyObject *value; diff --git a/Python/critical_section.c b/Python/critical_section.c index e37a498092ae8b..859e3537ed3319 100644 --- a/Python/critical_section.c +++ b/Python/critical_section.c @@ -202,7 +202,7 @@ PyCriticalSection2_End(PyCriticalSection2 *c) #endif } -void +int _PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message) { #ifdef Py_GIL_DISABLED @@ -212,12 +212,13 @@ _PyCriticalSection_WarnIfNotHeld(PyObject *op, const char *message) if (prev & _Py_CRITICAL_SECTION_TWO_MUTEXES) { PyCriticalSection2 *cs = (PyCriticalSection2 *)(prev & ~_Py_CRITICAL_SECTION_MASK); if (cs == NULL || (cs->_cs_base._cs_mutex != mutex && cs->_cs_mutex2 != mutex)) - PyErr_WarnEx(NULL, message, 2); + return PyErr_WarnEx(NULL, message, 2); } else { PyCriticalSection *cs = (PyCriticalSection *)(prev & ~_Py_CRITICAL_SECTION_MASK); if (cs == NULL || cs->_cs_mutex != mutex) - PyErr_WarnEx(NULL, message, 2); + return PyErr_WarnEx(NULL, message, 2); } #endif + return 0; } From dced20cb6b9f172cbe7ab0896ef85c28a13d63e2 Mon Sep 17 00:00:00 2001 From: Changaco Date: Thu, 23 Apr 2026 19:18:33 +0000 Subject: [PATCH 13/13] catch the new warning when testing --- Lib/test/test_defaultdict.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_defaultdict.py b/Lib/test/test_defaultdict.py index 5d3cc3bf72983c..4fb01486891f4d 100644 --- a/Lib/test/test_defaultdict.py +++ b/Lib/test/test_defaultdict.py @@ -2,7 +2,9 @@ import copy import pickle +import sys import unittest +import warnings from collections import defaultdict @@ -48,14 +50,16 @@ def test_basic(self): self.assertRaises(TypeError, defaultdict, 1) def test_missing(self): - d1 = defaultdict() - self.assertRaises(KeyError, d1.__missing__, 42) - d1.default_factory = list - v1 = d1.__missing__(42) - self.assertEqual(v1, []) - v2 = d1.__missing__(42) - self.assertEqual(v2, []) - self.assertIsNot(v2, v1) + with warnings.catch_warnings(record=True, action='always') as w: + d1 = defaultdict() + self.assertRaises(KeyError, d1.__missing__, 42) + d1.default_factory = list + v1 = d1.__missing__(42) + self.assertEqual(v1, []) + v2 = d1.__missing__(42) + self.assertEqual(v2, []) + self.assertIsNot(v2, v1) + self.assertEqual(len(w), 0 if sys._is_gil_enabled() else 3) def test_repr(self): d1 = defaultdict()