From 4a9f0fa85f7574084ff000e4ad636af0d875991a Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 2 Nov 2026 12:10:46 +0100 Subject: [PATCH 1/4] Use a managed dict for the _io types Replace the explicit dict slot of _io._IOBase and its C subclasses with Py_TPFLAGS_MANAGED_DICT, so instance attributes are stored as inline values with shared keys instead of a per-instance dict, and attribute access can be specialized. The flag is inherited by all C and Python subclasses. Co-Authored-By: Claude Fable 5.1 --- Lib/test/test_io/test_general.py | 35 ++++++++++++++++++++++++++++ Lib/test/test_io/test_memoryio.py | 7 ++++-- Modules/_io/bufferedio.c | 16 ++++--------- Modules/_io/bytesio.c | 38 ++++++++++++++++--------------- Modules/_io/fileio.c | 8 ++----- Modules/_io/iobase.c | 16 +++++-------- Modules/_io/stringio.c | 34 +++++++++++++++------------ Modules/_io/textio.c | 7 ++---- Modules/_io/winconsoleio.c | 10 +++----- 9 files changed, 96 insertions(+), 75 deletions(-) diff --git a/Lib/test/test_io/test_general.py b/Lib/test/test_io/test_general.py index c4d5a80086718b9..8573a1620317762 100644 --- a/Lib/test/test_io/test_general.py +++ b/Lib/test/test_io/test_general.py @@ -895,6 +895,41 @@ def test_immutable_types(self): with self.assertRaisesRegex(TypeError, "immutable"): tp.foo = "bar" + def test_managed_dict(self): + # The _io types use Py_TPFLAGS_MANAGED_DICT: there is no explicit + # dict slot and instance attributes are stored as inline values. + _testinternalcapi = import_helper.import_module("_testinternalcapi") + _io = self._io + no_dict = {_io.IncrementalNewlineDecoder, _io._BytesIOBuffer} + for tp in self.types: + with self.subTest(tp=tp): + if tp in no_dict: + self.assertEqual(tp.__dictoffset__, 0) + else: + self.assertEqual(tp.__dictoffset__, -1) + + # Use fresh subclasses: each class has its own shared keys, which + # may already be exhausted on the base types by earlier tests. + class BytesIO(_io.BytesIO): pass + class StringIO(_io.StringIO): pass + class BufferedReader(_io.BufferedReader): pass + class TextIOWrapper(_io.TextIOWrapper): pass + class RawIO(_io._RawIOBase): + def __init__(self): + self.attr = 1 + instances = [ + BytesIO(b"data"), + StringIO("data"), + BufferedReader(_io.BytesIO(b"data")), + TextIOWrapper(_io.BytesIO()), + RawIO(), + ] + for obj in instances: + with self.subTest(obj=type(obj).__name__): + obj.attr = 1 + self.assertTrue(_testinternalcapi.has_inline_values(obj)) + self.assertEqual(obj.__dict__["attr"], 1) + def test_class_hierarchy(self): def check_subs(types, base): for tp in types: diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 89b0e4c9463ac0a..9559d06a2a1bc81 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -10,6 +10,7 @@ import io import _pyio as pyio import pickle +import struct import sys import weakref @@ -1037,11 +1038,13 @@ def test_setstate(self): @support.cpython_only def test_sizeof(self): if support.Py_GIL_DISABLED: - basesize = support.calcobjsize('P2n2Pni') + basesize = support.calcobjsize('P2nPni') else: - basesize = support.calcobjsize('P2n2Pn') + basesize = support.calcobjsize('P2nPn') check = self.check_sizeof self.assertEqual(object.__sizeof__(io.BytesIO()), basesize) + # sys.getsizeof() includes the managed dict pre-header + basesize += 2 * struct.calcsize('P') check(io.BytesIO(), basesize ) n = 1000 # use a variable to prevent constant folding check(io.BytesIO(b'a' * n), basesize + sys.getsizeof(b'a' * n)) diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 44c3f56caee18b6..77f3e1d0a7994fd 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -260,7 +260,6 @@ typedef struct { Py_ssize_t buffer_size; Py_ssize_t buffer_mask; - PyObject *dict; PyObject *weakreflist; } buffered; @@ -417,7 +416,7 @@ buffered_clear(PyObject *op) buffered *self = buffered_CAST(op); self->ok = 0; Py_CLEAR(self->raw); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -467,8 +466,7 @@ buffered_traverse(PyObject *op, visitproc visit, void *arg) buffered *self = buffered_CAST(op); Py_VISIT(Py_TYPE(self)); Py_VISIT(self->raw); - Py_VISIT(self->dict); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } /* Because this can call arbitrary code, it shouldn't be called when @@ -2250,7 +2248,6 @@ typedef struct { PyObject_HEAD buffered *reader; buffered *writer; - PyObject *dict; PyObject *weakreflist; } rwpair; @@ -2309,10 +2306,9 @@ bufferedrwpair_traverse(PyObject *op, visitproc visit, void *arg) { rwpair *self = rwpair_CAST(op); Py_VISIT(Py_TYPE(self)); - Py_VISIT(self->dict); Py_VISIT(self->reader); Py_VISIT(self->writer); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static int @@ -2321,7 +2317,7 @@ bufferedrwpair_clear(PyObject *op) rwpair *self = rwpair_CAST(op); Py_CLEAR(self->reader); Py_CLEAR(self->writer); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -2639,7 +2635,6 @@ static PyMemberDef bufferedreader_members[] = { {"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(buffered, dict), Py_READONLY}, {NULL} }; @@ -2698,7 +2693,6 @@ static PyMemberDef bufferedwriter_members[] = { {"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(buffered, dict), Py_READONLY}, {NULL} }; @@ -2752,7 +2746,6 @@ static PyMethodDef bufferedrwpair_methods[] = { static PyMemberDef bufferedrwpair_members[] = { {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(rwpair, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(rwpair, dict), Py_READONLY}, {NULL} }; @@ -2815,7 +2808,6 @@ static PyMemberDef bufferedrandom_members[] = { {"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(buffered, dict), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 2571da4538363f1..cf8a443567833b4 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -21,7 +21,6 @@ typedef struct { PyObject *buf; Py_ssize_t pos; Py_ssize_t string_size; - PyObject *dict; PyObject *weakreflist; Py_ssize_t exports; #ifdef Py_GIL_DISABLED @@ -974,11 +973,16 @@ _io_BytesIO___getstate___impl(bytesio *self) if (initvalue == NULL) return NULL; - if (self->dict == NULL) { - dict = Py_NewRef(Py_None); + dict = PyObject_GenericGetDict((PyObject *)self, NULL); + if (dict == NULL) { + Py_DECREF(initvalue); + return NULL; + } + if (PyDict_GET_SIZE(dict) == 0) { + Py_SETREF(dict, Py_NewRef(Py_None)); } else { - dict = PyDict_Copy(self->dict); + Py_SETREF(dict, PyDict_Copy(dict)); if (dict == NULL) { Py_DECREF(initvalue); return NULL; @@ -1061,16 +1065,16 @@ _io_BytesIO___setstate___impl(bytesio *self, PyObject *state) Py_TYPE(dict)->tp_name); return NULL; } - if (self->dict) { - /* Alternatively, we could replace the internal dictionary - completely. However, it seems more practical to just update it. */ - if (PyDict_Update(self->dict, dict) < 0) - return NULL; + /* Alternatively, we could replace the internal dictionary + completely. However, it seems more practical to just update it. */ + PyObject *self_dict = PyObject_GenericGetDict((PyObject *)self, NULL); + if (self_dict == NULL) { + return NULL; } - else { - /* The LOAD_ATTR specializations read the dict slot lock-free - with an acquire load, so pair it with a release store. */ - FT_ATOMIC_STORE_PTR_RELEASE(self->dict, Py_NewRef(dict)); + int rc = PyDict_Update(self_dict, dict); + Py_DECREF(self_dict); + if (rc < 0) { + return NULL; } } @@ -1089,7 +1093,7 @@ bytesio_dealloc(PyObject *op) PyErr_Print(); } Py_CLEAR(self->buf); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); FT_CLEAR_WEAKREFS(op, self->weakreflist); tp->tp_free(self); Py_DECREF(tp); @@ -1186,16 +1190,15 @@ bytesio_traverse(PyObject *op, visitproc visit, void *arg) { bytesio *self = bytesio_CAST(op); Py_VISIT(Py_TYPE(self)); - Py_VISIT(self->dict); Py_VISIT(self->buf); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static int bytesio_clear(PyObject *op) { bytesio *self = bytesio_CAST(op); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); if (FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) == 0) { Py_CLEAR(self->buf); } @@ -1240,7 +1243,6 @@ static struct PyMethodDef bytesio_methods[] = { static PyMemberDef bytesio_members[] = { {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(bytesio, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(bytesio, dict), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index e5cd7aca5de2f17..ad08c6cbfc24ddb 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -81,7 +81,6 @@ typedef struct { gh-109523). */ struct _Py_stat_struct *stat_atopen; PyObject *weakreflist; - PyObject *dict; } fileio; #define PyFileIO_Check(state, op) (PyObject_TypeCheck((op), state->PyFileIO_Type)) @@ -558,15 +557,13 @@ fileio_traverse(PyObject *op, visitproc visit, void *arg) { fileio *self = PyFileIO_CAST(op); Py_VISIT(Py_TYPE(self)); - Py_VISIT(self->dict); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static int fileio_clear(PyObject *op) { - fileio *self = PyFileIO_CAST(op); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -1367,7 +1364,6 @@ static PyGetSetDef fileio_getsetlist[] = { static PyMemberDef fileio_members[] = { {"_finalizing", Py_T_BOOL, offsetof(fileio, finalizing), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(fileio, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(fileio, dict), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c index 2b482687e85c5bc..9cef2fe885cc947 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -33,7 +33,6 @@ class _io._RawIOBase "PyObject *" "clinic_state()->PyRawIOBase_Type" typedef struct { PyObject_HEAD - PyObject *dict; PyObject *weakreflist; } iobase; @@ -383,15 +382,13 @@ iobase_traverse(PyObject *op, visitproc visit, void *arg) { iobase *self = iobase_CAST(op); Py_VISIT(Py_TYPE(self)); - Py_VISIT(self->dict); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static int iobase_clear(PyObject *op) { - iobase *self = iobase_CAST(op); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -400,8 +397,8 @@ iobase_clear(PyObject *op) static void iobase_dealloc(PyObject *op) { - /* NOTE: since IOBaseObject has its own dict, Python-defined attributes - are still available here for close() to use. + /* NOTE: since the managed dict is only cleared below, Python-defined + attributes are still available here for close() to use. However, if the derived class declares a __slots__, those slots are already gone. */ @@ -417,7 +414,7 @@ iobase_dealloc(PyObject *op) PyTypeObject *tp = Py_TYPE(self); _PyObject_GC_UNTRACK(self); FT_CLEAR_WEAKREFS(op, self->weakreflist); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); tp->tp_free(self); Py_DECREF(tp); } @@ -909,7 +906,6 @@ static PyGetSetDef iobase_getset[] = { static struct PyMemberDef iobase_members[] = { {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(iobase, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(iobase, dict), Py_READONLY}, {NULL}, }; @@ -932,7 +928,7 @@ PyType_Spec _Py_iobase_spec = { .name = "_io._IOBase", .basicsize = sizeof(iobase), .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC | - Py_TPFLAGS_IMMUTABLETYPE), + Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_DICT), .slots = iobase_slots, }; diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index 25ebe8909278487..1d2c5074652d0cc 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -42,7 +42,6 @@ typedef struct { PyObject *readnl; PyObject *writenl; - PyObject *dict; PyObject *weakreflist; _PyIO_State *module_state; } stringio; @@ -675,8 +674,7 @@ stringio_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(self->readnl); Py_VISIT(self->writenl); Py_VISIT(self->decoder); - Py_VISIT(self->dict); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static int @@ -686,7 +684,7 @@ stringio_clear(PyObject *op) Py_CLEAR(self->readnl); Py_CLEAR(self->writenl); Py_CLEAR(self->decoder); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -930,11 +928,16 @@ _io_StringIO___getstate___impl(stringio *self) if (initvalue == NULL) return NULL; - if (self->dict == NULL) { - dict = Py_NewRef(Py_None); + dict = PyObject_GenericGetDict((PyObject *)self, NULL); + if (dict == NULL) { + Py_DECREF(initvalue); + return NULL; + } + if (PyDict_GET_SIZE(dict) == 0) { + Py_SETREF(dict, Py_NewRef(Py_None)); } else { - dict = PyDict_Copy(self->dict); + Py_SETREF(dict, PyDict_Copy(dict)); if (dict == NULL) { Py_DECREF(initvalue); return NULL; @@ -1045,14 +1048,16 @@ _io_StringIO___setstate___impl(stringio *self, PyObject *state) Py_TYPE(dict)->tp_name); return NULL; } - if (self->dict) { - /* Alternatively, we could replace the internal dictionary - completely. However, it seems more practical to just update it. */ - if (PyDict_Update(self->dict, dict) < 0) - return NULL; + /* Alternatively, we could replace the internal dictionary + completely. However, it seems more practical to just update it. */ + PyObject *self_dict = PyObject_GenericGetDict((PyObject *)self, NULL); + if (self_dict == NULL) { + return NULL; } - else { - self->dict = Py_NewRef(dict); + int rc = PyDict_Update(self_dict, dict); + Py_DECREF(self_dict); + if (rc < 0) { + return NULL; } } @@ -1140,7 +1145,6 @@ static PyGetSetDef stringio_getset[] = { static struct PyMemberDef stringio_members[] = { {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(stringio, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(stringio, dict), Py_READONLY}, {NULL}, }; diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 4ef5251d451a4cd..e52b8d1d4664925 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -732,7 +732,6 @@ struct textio PyObject *raw; PyObject *weakreflist; - PyObject *dict; _PyIO_State *state; }; @@ -1542,7 +1541,7 @@ textiowrapper_clear(PyObject *op) Py_CLEAR(self->errors); Py_CLEAR(self->raw); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -1578,8 +1577,7 @@ textiowrapper_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(self->errors); Py_VISIT(self->raw); - Py_VISIT(self->dict); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static PyObject * @@ -3539,7 +3537,6 @@ static PyMemberDef textiowrapper_members[] = { {"write_through", Py_T_BOOL, offsetof(textio, write_through), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(textio, finalizing), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(textio, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(textio, dict), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/winconsoleio.c b/Modules/_io/winconsoleio.c index 3da4f1df1708a78..8d32b49e44830a5 100644 --- a/Modules/_io/winconsoleio.c +++ b/Modules/_io/winconsoleio.c @@ -217,7 +217,6 @@ typedef struct { char finalizing; unsigned int blksize; PyObject *weakreflist; - PyObject *dict; char buf[SMALLBUF]; wchar_t wbuf; } winconsoleio; @@ -498,15 +497,13 @@ winconsoleio_traverse(PyObject *op, visitproc visit, void *arg) { winconsoleio *self = winconsoleio_CAST(op); Py_VISIT(Py_TYPE(self)); - Py_VISIT(self->dict); - return 0; + return PyObject_VisitManagedDict(op, visit, arg); } static int winconsoleio_clear(PyObject *op) { - winconsoleio *self = winconsoleio_CAST(op); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); return 0; } @@ -520,7 +517,7 @@ winconsoleio_dealloc(PyObject *op) return; _PyObject_GC_UNTRACK(self); FT_CLEAR_WEAKREFS(op, self->weakreflist); - Py_CLEAR(self->dict); + PyObject_ClearManagedDict(op); tp->tp_free(self); Py_DECREF(tp); } @@ -1259,7 +1256,6 @@ static PyMemberDef winconsoleio_members[] = { {"_blksize", Py_T_UINT, offsetof(winconsoleio, blksize), 0}, {"_finalizing", Py_T_BOOL, offsetof(winconsoleio, finalizing), 0}, {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(winconsoleio, weakreflist), Py_READONLY}, - {"__dictoffset__", Py_T_PYSSIZET, offsetof(winconsoleio, dict), Py_READONLY}, {NULL} }; From 899dcc9ccca970ba18355465a335e4b782db09a1 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 3 Nov 2026 09:08:15 +0100 Subject: [PATCH 2/4] Use a managed weakref list for the _io types Replace the explicit weakreflist field of the _io types with Py_TPFLAGS_MANAGED_WEAKREF, inherited from _IOBase, so the weakref list lives in the pre-header like the managed dict. Co-Authored-By: Claude Fable 5.1 --- Lib/test/test_io/test_memoryio.py | 6 +++--- Modules/_io/bufferedio.c | 12 +++--------- Modules/_io/bytesio.c | 5 +---- Modules/_io/fileio.c | 6 +----- Modules/_io/iobase.c | 8 +++----- Modules/_io/stringio.c | 6 ++---- Modules/_io/textio.c | 5 +---- Modules/_io/winconsoleio.c | 6 +----- 8 files changed, 15 insertions(+), 39 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 9559d06a2a1bc81..20c2777b6c3ce07 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -1038,12 +1038,12 @@ def test_setstate(self): @support.cpython_only def test_sizeof(self): if support.Py_GIL_DISABLED: - basesize = support.calcobjsize('P2nPni') + basesize = support.calcobjsize('P2nni') else: - basesize = support.calcobjsize('P2nPn') + basesize = support.calcobjsize('P2nn') check = self.check_sizeof self.assertEqual(object.__sizeof__(io.BytesIO()), basesize) - # sys.getsizeof() includes the managed dict pre-header + # sys.getsizeof() includes the managed dict and weakref pre-header basesize += 2 * struct.calcsize('P') check(io.BytesIO(), basesize ) n = 1000 # use a variable to prevent constant folding diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c index 77f3e1d0a7994fd..869f245abcd41de 100644 --- a/Modules/_io/bufferedio.c +++ b/Modules/_io/bufferedio.c @@ -9,11 +9,11 @@ #include "Python.h" #include "pycore_call.h" // _PyObject_CallNoArgs() +#include "pycore_critical_section.h" // _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED() #include "pycore_fileutils.h" // _PyFile_Flush #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_pyerrors.h" // _Py_FatalErrorFormat() #include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #include "_iomodule.h" @@ -260,7 +260,6 @@ typedef struct { Py_ssize_t buffer_size; Py_ssize_t buffer_mask; - PyObject *weakreflist; } buffered; #define buffered_CAST(op) ((buffered *)(op)) @@ -430,7 +429,7 @@ buffered_dealloc(PyObject *op) return; _PyObject_GC_UNTRACK(self); self->ok = 0; - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); if (self->buffer) { PyMem_Free(self->buffer); self->buffer = NULL; @@ -2248,7 +2247,6 @@ typedef struct { PyObject_HEAD buffered *reader; buffered *writer; - PyObject *weakreflist; } rwpair; #define rwpair_CAST(op) ((rwpair *)(op)) @@ -2327,7 +2325,7 @@ bufferedrwpair_dealloc(PyObject *op) rwpair *self = rwpair_CAST(op); PyTypeObject *tp = Py_TYPE(self); _PyObject_GC_UNTRACK(self); - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); (void)bufferedrwpair_clear(op); tp->tp_free(self); Py_DECREF(tp); @@ -2634,7 +2632,6 @@ static PyMethodDef bufferedreader_methods[] = { static PyMemberDef bufferedreader_members[] = { {"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0}, - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY}, {NULL} }; @@ -2692,7 +2689,6 @@ static PyMethodDef bufferedwriter_methods[] = { static PyMemberDef bufferedwriter_members[] = { {"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0}, - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY}, {NULL} }; @@ -2745,7 +2741,6 @@ static PyMethodDef bufferedrwpair_methods[] = { }; static PyMemberDef bufferedrwpair_members[] = { - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(rwpair, weakreflist), Py_READONLY}, {NULL} }; @@ -2807,7 +2802,6 @@ static PyMethodDef bufferedrandom_methods[] = { static PyMemberDef bufferedrandom_members[] = { {"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0}, - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index cf8a443567833b4..89b49f725a1fa34 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -5,7 +5,6 @@ #include "pycore_object.h" #include "pycore_pyatomic_ft_wrappers.h" #include "pycore_sysmodule.h" // _PySys_GetSizeOf() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #include // offsetof() #include "_iomodule.h" @@ -21,7 +20,6 @@ typedef struct { PyObject *buf; Py_ssize_t pos; Py_ssize_t string_size; - PyObject *weakreflist; Py_ssize_t exports; #ifdef Py_GIL_DISABLED int buf_shared; @@ -1094,7 +1092,7 @@ bytesio_dealloc(PyObject *op) } Py_CLEAR(self->buf); PyObject_ClearManagedDict(op); - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); tp->tp_free(self); Py_DECREF(tp); } @@ -1242,7 +1240,6 @@ static struct PyMethodDef bytesio_methods[] = { }; static PyMemberDef bytesio_members[] = { - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(bytesio, weakreflist), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index ad08c6cbfc24ddb..ecbad302ce195cf 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -4,7 +4,6 @@ #include "pycore_fileutils.h" // _Py_BEGIN_SUPPRESS_IPH #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #include // bool #ifdef HAVE_UNISTD_H @@ -80,7 +79,6 @@ typedef struct { modified outside of the fileio object / Python (ex. gh-90102, GH-121941, gh-109523). */ struct _Py_stat_struct *stat_atopen; - PyObject *weakreflist; } fileio; #define PyFileIO_Check(state, op) (PyObject_TypeCheck((op), state->PyFileIO_Type)) @@ -219,7 +217,6 @@ fileio_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->truncate = 0; self->stat_atopen = NULL; self->closefd = 1; - self->weakreflist = NULL; return (PyObject *) self; } @@ -581,7 +578,7 @@ fileio_dealloc(PyObject *op) PyMem_Free(self->stat_atopen); self->stat_atopen = NULL; } - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); (void)fileio_clear(op); PyTypeObject *tp = Py_TYPE(op); @@ -1363,7 +1360,6 @@ static PyGetSetDef fileio_getsetlist[] = { static PyMemberDef fileio_members[] = { {"_finalizing", Py_T_BOOL, offsetof(fileio, finalizing), 0}, - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(fileio, weakreflist), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c index 9cef2fe885cc947..0b4292081ff520b 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -14,7 +14,6 @@ #include "pycore_long.h" // _PyLong_GetOne() #include "pycore_object.h" // _PyType_HasFeature() #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #include // offsetof() #include "_iomodule.h" @@ -33,7 +32,6 @@ class _io._RawIOBase "PyObject *" "clinic_state()->PyRawIOBase_Type" typedef struct { PyObject_HEAD - PyObject *weakreflist; } iobase; #define iobase_CAST(op) ((iobase *)(op)) @@ -413,7 +411,7 @@ iobase_dealloc(PyObject *op) } PyTypeObject *tp = Py_TYPE(self); _PyObject_GC_UNTRACK(self); - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); PyObject_ClearManagedDict(op); tp->tp_free(self); Py_DECREF(tp); @@ -905,7 +903,6 @@ static PyGetSetDef iobase_getset[] = { }; static struct PyMemberDef iobase_members[] = { - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(iobase, weakreflist), Py_READONLY}, {NULL}, }; @@ -928,7 +925,8 @@ PyType_Spec _Py_iobase_spec = { .name = "_io._IOBase", .basicsize = sizeof(iobase), .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC | - Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_DICT), + Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_DICT | + Py_TPFLAGS_MANAGED_WEAKREF), .slots = iobase_slots, }; diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index 1d2c5074652d0cc..a80266c13980ab8 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -1,8 +1,8 @@ #include "Python.h" #include // offsetof() +#include "pycore_critical_section.h" // _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED() #include "pycore_object.h" #include "pycore_unicodeobject.h" // _PyUnicode_ResizeCompact() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #include "_iomodule.h" /* Implementation note: the buffer is always at least one character longer @@ -42,7 +42,6 @@ typedef struct { PyObject *readnl; PyObject *writenl; - PyObject *weakreflist; _PyIO_State *module_state; } stringio; @@ -701,7 +700,7 @@ stringio_dealloc(PyObject *op) } PyUnicodeWriter_Discard(self->writer); (void)stringio_clear(op); - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); tp->tp_free(self); Py_DECREF(tp); } @@ -1144,7 +1143,6 @@ static PyGetSetDef stringio_getset[] = { }; static struct PyMemberDef stringio_members[] = { - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(stringio, weakreflist), Py_READONLY}, {NULL}, }; diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index e52b8d1d4664925..4a6eca87c46c102 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -16,7 +16,6 @@ #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_unicodeobject.h" // _PyUnicode_AsASCIIString() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #include "_iomodule.h" @@ -731,7 +730,6 @@ struct textio /* Cache raw object if it's a FileIO object */ PyObject *raw; - PyObject *weakreflist; _PyIO_State *state; }; @@ -1555,7 +1553,7 @@ textiowrapper_dealloc(PyObject *op) return; self->ok = 0; _PyObject_GC_UNTRACK(self); - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); (void)textiowrapper_clear(op); tp->tp_free(self); Py_DECREF(tp); @@ -3536,7 +3534,6 @@ static PyMemberDef textiowrapper_members[] = { {"line_buffering", Py_T_BOOL, offsetof(textio, line_buffering), Py_READONLY}, {"write_through", Py_T_BOOL, offsetof(textio, write_through), Py_READONLY}, {"_finalizing", Py_T_BOOL, offsetof(textio, finalizing), 0}, - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(textio, weakreflist), Py_READONLY}, {NULL} }; diff --git a/Modules/_io/winconsoleio.c b/Modules/_io/winconsoleio.c index 8d32b49e44830a5..89e743f4e579700 100644 --- a/Modules/_io/winconsoleio.c +++ b/Modules/_io/winconsoleio.c @@ -10,7 +10,6 @@ #include "pycore_fileutils.h" // _Py_BEGIN_SUPPRESS_IPH #include "pycore_object.h" // _PyObject_GC_UNTRACK() #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() -#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() #ifdef HAVE_WINDOWS_CONSOLE_IO @@ -216,7 +215,6 @@ typedef struct { unsigned int closefd : 1; char finalizing; unsigned int blksize; - PyObject *weakreflist; char buf[SMALLBUF]; wchar_t wbuf; } winconsoleio; @@ -299,7 +297,6 @@ winconsoleio_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->writable = 0; self->closefd = 0; self->blksize = 0; - self->weakreflist = NULL; } return (PyObject *) self; @@ -516,7 +513,7 @@ winconsoleio_dealloc(PyObject *op) if (_PyIOBase_finalize(op) < 0) return; _PyObject_GC_UNTRACK(self); - FT_CLEAR_WEAKREFS(op, self->weakreflist); + PyObject_ClearWeakRefs(op); PyObject_ClearManagedDict(op); tp->tp_free(self); Py_DECREF(tp); @@ -1255,7 +1252,6 @@ static PyGetSetDef winconsoleio_getsetlist[] = { static PyMemberDef winconsoleio_members[] = { {"_blksize", Py_T_UINT, offsetof(winconsoleio, blksize), 0}, {"_finalizing", Py_T_BOOL, offsetof(winconsoleio, finalizing), 0}, - {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(winconsoleio, weakreflist), Py_READONLY}, {NULL} }; From 7e36ce3a28737b13eaef1a6032b6ae246f02b05c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:21:13 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst diff --git a/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst b/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst new file mode 100644 index 000000000000000..ccf469877933b02 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst @@ -0,0 +1,3 @@ +Speed up attribute access on instances of :mod:`io` classes and their +subclasses by storing instance attributes and weak references in the object pre-header. The ``__dictoffset__`` and +``__weakrefoffset__`` attributes of these classes now report the managed offsets, and C extension types that subclass an :mod:`io` class can no longer declare their own ``__dictoffset__`` or ``__weaklistoffset__`` member. From 0d2c8f1d7d801c49f6c7ff9a41bf5f9425a3c246 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 3 Nov 2026 09:29:31 +0100 Subject: [PATCH 4/4] Fix line endings of the NEWS entry Co-Authored-By: Claude Fable 5.1 --- .../2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst b/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst index ccf469877933b02..3b15925773b99ec 100644 --- a/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst +++ b/Misc/NEWS.d/next/Library/2026-11-03-08-21-06.gh-issue-161184.PIhcD9.rst @@ -1,3 +1,6 @@ -Speed up attribute access on instances of :mod:`io` classes and their -subclasses by storing instance attributes and weak references in the object pre-header. The ``__dictoffset__`` and -``__weakrefoffset__`` attributes of these classes now report the managed offsets, and C extension types that subclass an :mod:`io` class can no longer declare their own ``__dictoffset__`` or ``__weaklistoffset__`` member. +Speed up attribute access on instances of :mod:`io` classes and their +subclasses by storing instance attributes and weak references in the object +pre-header. The ``__dictoffset__`` and ``__weakrefoffset__`` attributes +of these classes now report the managed offsets, and C extension types that +subclass an :mod:`io` class can no longer declare their own ``__dictoffset__`` +or ``__weaklistoffset__`` member.