Skip to content

Commit c21b016

Browse files
committed
pythongh-95388: Deprecate creating immutable types with mutable bases
1 parent 664e96a commit c21b016

File tree

5 files changed

+78
-0
lines changed

5 files changed

+78
-0
lines changed

Doc/whatsnew/3.12.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ Pending Removal in Python 3.14
205205

206206
(Contributed by Jason R. Coombs and Hugo van Kemenade in :gh:`93963`.)
207207

208+
* Creating :c:const:`immutable types <Py_TPFLAGS_IMMUTABLETYPE>` with mutable
209+
bases using the C API.
210+
211+
208212
Pending Removal in Future Versions
209213
----------------------------------
210214

@@ -458,6 +462,9 @@ Deprecated
458462
:c:type:`PyConfig` instead.
459463
(Contributed by Victor Stinner in :gh:`77782`.)
460464

465+
* Creating :c:const:`immutable types <Py_TPFLAGS_IMMUTABLETYPE>` with mutable
466+
bases is deprecated and will be disabled in Python 3.14.
467+
461468

462469
Removed
463470
-------

Lib/test/test_capi.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import time
1717
import unittest
1818
import weakref
19+
import warnings
1920
from test import support
2021
from test.support import MISSING_C_DOCSTRINGS
2122
from test.support import import_helper
@@ -625,6 +626,32 @@ def test_pytype_fromspec_with_repeated_slots(self):
625626
with self.assertRaises(SystemError):
626627
_testcapi.create_type_from_repeated_slots(variant)
627628

629+
def test_immutable_type_with_mutable_base(self):
630+
# Add deprecation warning here so it's removed in 3.14
631+
warnings._deprecated(
632+
'creating immutable classes with mutable bases', remove=(3, 14))
633+
634+
class MutableBase:
635+
def meth(self):
636+
return 'original'
637+
638+
with self.assertWarns(DeprecationWarning):
639+
ImmutableSubclass = _testcapi.make_immutable_type_with_base(
640+
MutableBase)
641+
instance = ImmutableSubclass()
642+
643+
self.assertEqual(instance.meth(), 'original')
644+
645+
# Cannot override the static type's method
646+
with self.assertRaises(TypeError):
647+
ImmutableSubclass.meth = lambda self: 'overridden'
648+
self.assertEqual(instance.meth(), 'original')
649+
650+
# Can change the method on the mutable base
651+
MutableBase.meth = lambda self: 'changed'
652+
self.assertEqual(instance.meth(), 'changed')
653+
654+
628655
def test_pynumber_tobase(self):
629656
from _testcapi import pynumber_tobase
630657
self.assertEqual(pynumber_tobase(123, 2), '0b1111011')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Creating :c:const:`immutable types <Py_TPFLAGS_IMMUTABLETYPE>` with mutable
2+
bases is deprecated and is planned to be disabled in Python 3.14.

Modules/_testcapi/heaptype.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,21 @@ create_type_from_repeated_slots(PyObject *self, PyObject *variant_obj)
365365
}
366366

367367

368+
369+
static PyObject *
370+
make_immutable_type_with_base(PyObject *self, PyObject *base)
371+
{
372+
assert(PyType_Check(base));
373+
PyType_Spec ImmutableSubclass_spec = {
374+
.name = "ImmutableSubclass",
375+
.basicsize = ((PyTypeObject*)base)->tp_basicsize,
376+
.slots = empty_type_slots,
377+
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE,
378+
};
379+
return PyType_FromSpecWithBases(&ImmutableSubclass_spec, base);
380+
}
381+
382+
368383
static PyMethodDef TestMethods[] = {
369384
{"pytype_fromspec_meta", pytype_fromspec_meta, METH_O},
370385
{"test_type_from_ephemeral_spec", test_type_from_ephemeral_spec, METH_NOARGS},
@@ -375,6 +390,7 @@ static PyMethodDef TestMethods[] = {
375390
{"test_from_spec_invalid_metatype_inheritance",
376391
test_from_spec_invalid_metatype_inheritance,
377392
METH_NOARGS},
393+
{"make_immutable_type_with_base", make_immutable_type_with_base, METH_O},
378394
{NULL},
379395
};
380396

Objects/typeobject.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3666,6 +3666,32 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
36663666
goto finally;
36673667
}
36683668

3669+
/* If this is an immutable type, check if all bases are also immutable,
3670+
* and (for now) fire a deprecation warning if not.
3671+
* (This isn't necessary for static types: those can't have heap bases,
3672+
* and only heap types can be mutable.)
3673+
*/
3674+
if (spec->flags & Py_TPFLAGS_IMMUTABLETYPE) {
3675+
for (int i=0; i<PyTuple_Size(bases); i++) {
3676+
PyTypeObject *b = (PyTypeObject*)PyTuple_GetItem(bases, i);
3677+
if (!b) {
3678+
goto finally;
3679+
}
3680+
if (!_PyType_HasFeature(b, Py_TPFLAGS_IMMUTABLETYPE)) {
3681+
if (PyErr_WarnFormat(
3682+
PyExc_DeprecationWarning,
3683+
0,
3684+
"Creating immutable type %s from mutable base %s is "
3685+
"deprecated, and slated to be disallowed in Python 3.14.",
3686+
spec->name,
3687+
b->tp_name))
3688+
{
3689+
goto finally;
3690+
}
3691+
}
3692+
}
3693+
}
3694+
36693695
/* Calculate the metaclass */
36703696

36713697
if (!metaclass) {

0 commit comments

Comments
 (0)