https://github.com/python/cpython/commit/4d21297d288f17c0db775605e2543749840e46b2
commit: 4d21297d288f17c0db775605e2543749840e46b2
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-01-06T11:36:00+02:00
summary:
gh-41779: Allow defining any __slots__ for a class derived from tuple
(GH-141763)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst
M Doc/reference/datamodel.rst
M Doc/whatsnew/3.15.rst
M Include/descrobject.h
M Include/internal/pycore_descrobject.h
M Lib/test/test_descr.py
M Objects/typeobject.c
M Python/specialize.c
M Python/structmember.c
diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst
index d92972117a31f1..488fbc6b1f68cd 100644
--- a/Doc/reference/datamodel.rst
+++ b/Doc/reference/datamodel.rst
@@ -2617,7 +2617,7 @@ Notes on using *__slots__*:
* :exc:`TypeError` will be raised if *__slots__* other than *__dict__* and
*__weakref__* are defined for a class derived from a
:c:member:`"variable-length" built-in type <PyTypeObject.tp_itemsize>` such
as
- :class:`int`, :class:`bytes`, and :class:`tuple`.
+ :class:`int`, :class:`bytes`, and :class:`type`, except :class:`tuple`.
* Any non-string :term:`iterable` may be assigned to *__slots__*.
@@ -2642,6 +2642,7 @@ Notes on using *__slots__*:
.. versionchanged:: 3.15
Allowed defining the *__dict__* and *__weakref__* *__slots__* for any class.
+ Allowed defining any *__slots__* for a class derived from :class:`tuple`.
.. _class-customization:
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index d5a2e04958e8a8..39d6fb6572c834 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -398,6 +398,10 @@ Other language changes
for any class.
(Contributed by Serhiy Storchaka in :gh:`41779`.)
+* Allowed defining any :ref:`__slots__ <slots>` for a class derived from
+ :class:`tuple` (including classes created by :func:`collections.namedtuple`).
+ (Contributed by Serhiy Storchaka in :gh:`41779`.)
+
New modules
===========
diff --git a/Include/descrobject.h b/Include/descrobject.h
index fd66d17b497a31..340de4e0e1e6ff 100644
--- a/Include/descrobject.h
+++ b/Include/descrobject.h
@@ -80,10 +80,14 @@ struct PyMemberDef {
#define _Py_T_NONE 20 // Deprecated. Value is always None.
/* Flags */
-#define Py_READONLY 1
-#define Py_AUDIT_READ 2 // Added in 3.10, harmless no-op before that
-#define _Py_WRITE_RESTRICTED 4 // Deprecated, no-op. Do not reuse the value.
-#define Py_RELATIVE_OFFSET 8
+#define Py_READONLY (1 << 0)
+#define Py_AUDIT_READ (1 << 1) // Added in 3.10, harmless no-op
before that
+#define _Py_WRITE_RESTRICTED (1 << 2) // Deprecated, no-op. Do not reuse the
value.
+#define Py_RELATIVE_OFFSET (1 << 3)
+
+#ifndef Py_LIMITED_API
+# define _Py_AFTER_ITEMS (1 << 4) // For internal use.
+#endif
PyAPI_FUNC(PyObject *) PyMember_GetOne(const char *, PyMemberDef *);
PyAPI_FUNC(int) PyMember_SetOne(char *, PyMemberDef *, PyObject *);
diff --git a/Include/internal/pycore_descrobject.h
b/Include/internal/pycore_descrobject.h
index 3cec59a68a3d2b..6143f82176a1f2 100644
--- a/Include/internal/pycore_descrobject.h
+++ b/Include/internal/pycore_descrobject.h
@@ -22,6 +22,8 @@ typedef propertyobject _PyPropertyObject;
extern PyTypeObject _PyMethodWrapper_Type;
+extern void *_PyMember_GetOffset(PyObject *, PyMemberDef *);
+
#ifdef __cplusplus
}
#endif
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 82a48ad4d1aced..0dc61ca7fb0da3 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -1320,6 +1320,18 @@ class X(object):
with self.assertRaisesRegex(AttributeError, "'X' object has no
attribute 'a'"):
X().a
+ def test_slots_after_items(self):
+ class C(tuple):
+ __slots__ = ['a']
+ x = C((1, 2, 3))
+ self.assertNotHasAttr(x, "__dict__")
+ self.assertNotHasAttr(x, "a")
+ x.a = 42
+ self.assertEqual(x.a, 42)
+ del x.a
+ self.assertNotHasAttr(x, "a")
+ self.assertEqual(x, (1, 2, 3))
+
def test_slots_special(self):
# Testing __dict__ and __weakref__ in __slots__...
class D(object):
@@ -1422,6 +1434,9 @@ class W(base):
self.assertIs(weakref.ref(a)(), a)
self.assertEqual(a, base(arg))
+ @support.subTests('base', [int, bytes] +
+ ([_testcapi.HeapCCollection] if _testcapi else []))
+ def test_unsupported_slots(self, base):
with self.assertRaises(TypeError):
class X(base):
__slots__ = ['x']
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst
new file mode 100644
index 00000000000000..16e546890c085b
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-19-20-42-21.gh-issue-41779.Psz9Vo.rst
@@ -0,0 +1,3 @@
+Allowed defining any :ref:`__slots__ <slots>` for a class derived from
+:class:`tuple` (including classes created by
+:func:`collections.namedtuple`).
diff --git a/Objects/typeobject.c b/Objects/typeobject.c
index 7f5149aeece12b..77e5c3e9f9ec95 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -4,6 +4,7 @@
#include "pycore_abstract.h" // _PySequence_IterSearch()
#include "pycore_call.h" // _PyObject_VectorcallTstate()
#include "pycore_code.h" // CO_FAST_FREE
+#include "pycore_descrobject.h" // _PyMember_GetOffset()
#include "pycore_dict.h" // _PyDict_KeysSize()
#include "pycore_function.h" // _PyFunction_GetVersionForCurrentState()
#include "pycore_interpframe.h" // _PyInterpreterFrame
@@ -2578,7 +2579,7 @@ traverse_slots(PyTypeObject *type, PyObject *self,
visitproc visit, void *arg)
mp = _PyHeapType_GET_MEMBERS((PyHeapTypeObject *)type);
for (i = 0; i < n; i++, mp++) {
if (mp->type == Py_T_OBJECT_EX) {
- char *addr = (char *)self + mp->offset;
+ void *addr = _PyMember_GetOffset(self, mp);
PyObject *obj = *(PyObject **)addr;
if (obj != NULL) {
int err = visit(obj, arg);
@@ -2653,7 +2654,7 @@ clear_slots(PyTypeObject *type, PyObject *self)
mp = _PyHeapType_GET_MEMBERS((PyHeapTypeObject *)type);
for (i = 0; i < n; i++, mp++) {
if (mp->type == Py_T_OBJECT_EX && !(mp->flags & Py_READONLY)) {
- char *addr = (char *)self + mp->offset;
+ void *addr = _PyMember_GetOffset(self, mp);
PyObject *obj = *(PyObject **)addr;
if (obj != NULL) {
*(PyObject **)addr = NULL;
@@ -4641,7 +4642,11 @@ type_new_descriptors(const type_new_ctx *ctx,
PyTypeObject *type, PyObject *dict
if (et->ht_slots != NULL) {
PyMemberDef *mp = _PyHeapType_GET_MEMBERS(et);
Py_ssize_t nslot = PyTuple_GET_SIZE(et->ht_slots);
- if (ctx->base->tp_itemsize != 0) {
+ int after_items = (ctx->base->tp_itemsize != 0 &&
+ !(ctx->base->tp_flags & Py_TPFLAGS_ITEMS_AT_END));
+ if (ctx->base->tp_itemsize != 0 &&
+ !(ctx->base->tp_flags & Py_TPFLAGS_TUPLE_SUBCLASS))
+ {
PyErr_Format(PyExc_TypeError,
"arbitrary __slots__ not supported for subtype of
'%s'",
ctx->base->tp_name);
@@ -4655,6 +4660,9 @@ type_new_descriptors(const type_new_ctx *ctx,
PyTypeObject *type, PyObject *dict
}
mp->type = Py_T_OBJECT_EX;
mp->offset = slotoffset;
+ if (after_items) {
+ mp->flags |= _Py_AFTER_ITEMS;
+ }
/* __dict__ and __weakref__ are already filtered out */
assert(strcmp(mp->name, "__dict__") != 0);
diff --git a/Python/specialize.c b/Python/specialize.c
index 31b7fd364cd8be..62f0373a4c274d 100644
--- a/Python/specialize.c
+++ b/Python/specialize.c
@@ -141,6 +141,7 @@ _PyCode_Quicken(_Py_CODEUNIT *instructions, Py_ssize_t
size, int enable_counters
#define SPEC_FAIL_ATTR_METACLASS_OVERRIDDEN 34
#define SPEC_FAIL_ATTR_SPLIT_DICT 35
#define SPEC_FAIL_ATTR_DESCR_NOT_DEFERRED 36
+#define SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS 37
/* Binary subscr and store subscr */
@@ -812,6 +813,10 @@ do_specialize_instance_load_attr(PyObject* owner,
_Py_CODEUNIT* instr, PyObject*
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_EXPECTED_ERROR);
return -1;
}
+ if (dmem->flags & _Py_AFTER_ITEMS) {
+ SPECIALIZATION_FAIL(LOAD_ATTR,
SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS);
+ return -1;
+ }
if (dmem->flags & Py_AUDIT_READ) {
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_AUDITED_SLOT);
return -1;
@@ -1006,6 +1011,10 @@ _Py_Specialize_StoreAttr(_PyStackRef owner_st,
_Py_CODEUNIT *instr, PyObject *na
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_EXPECTED_ERROR);
goto fail;
}
+ if (dmem->flags & _Py_AFTER_ITEMS) {
+ SPECIALIZATION_FAIL(LOAD_ATTR,
SPEC_FAIL_ATTR_SLOT_AFTER_ITEMS);
+ goto fail;
+ }
if (dmem->flags & Py_READONLY) {
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_ATTR_READ_ONLY);
goto fail;
diff --git a/Python/structmember.c b/Python/structmember.c
index 574acf296157f3..b88e13ac0462b8 100644
--- a/Python/structmember.c
+++ b/Python/structmember.c
@@ -3,6 +3,7 @@
#include "Python.h"
#include "pycore_abstract.h" // _PyNumber_Index()
+#include "pycore_descrobject.h" // _PyMember_GetOffset()
#include "pycore_long.h" // _PyLong_IsNegative()
#include "pycore_object.h" // _Py_TryIncrefCompare(), FT_ATOMIC_*()
#include "pycore_critical_section.h"
@@ -20,6 +21,17 @@ member_get_object(const char *addr, const char *obj_addr,
PyMemberDef *l)
return v;
}
+void *
+_PyMember_GetOffset(PyObject *obj, PyMemberDef *mp)
+{
+ unsigned char *addr = (unsigned char *)obj + mp->offset;
+ if (mp->flags & _Py_AFTER_ITEMS) {
+ PyTypeObject *type = Py_TYPE(obj);
+ addr += _Py_SIZE_ROUND_UP(Py_SIZE(obj) * type->tp_itemsize,
SIZEOF_VOID_P);
+ }
+ return addr;
+}
+
PyObject *
PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
{
@@ -31,7 +43,7 @@ PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
return NULL;
}
- const char* addr = obj_addr + l->offset;
+ const void *addr = _PyMember_GetOffset((PyObject *)obj_addr, l);
switch (l->type) {
case Py_T_BOOL:
v = PyBool_FromLong(FT_ATOMIC_LOAD_CHAR_RELAXED(*(char*)addr));
@@ -80,7 +92,7 @@ PyMember_GetOne(const char *obj_addr, PyMemberDef *l)
v = PyUnicode_FromString((char*)addr);
break;
case Py_T_CHAR: {
- char char_val = FT_ATOMIC_LOAD_CHAR_RELAXED(*addr);
+ char char_val = FT_ATOMIC_LOAD_CHAR_RELAXED(*(char*)addr);
v = PyUnicode_FromStringAndSize(&char_val, 1);
break;
}
@@ -151,10 +163,8 @@ PyMember_SetOne(char *addr, PyMemberDef *l, PyObject *v)
return -1;
}
-#ifdef Py_GIL_DISABLED
- PyObject *obj = (PyObject *) addr;
-#endif
- addr += l->offset;
+ PyObject *obj = (PyObject *)addr;
+ addr = _PyMember_GetOffset(obj, l);
if ((l->flags & Py_READONLY))
{
_______________________________________________
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]