Skip to content

Commit a613fed

Browse files
authored
gh-95388: Deprecate creating immutable types with mutable bases (GH-95533)
1 parent 000c387 commit a613fed

File tree

5 files changed

+80
-0
lines changed

5 files changed

+80
-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:data:`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:data:`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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import threading
1616
import time
1717
import unittest
18+
import warnings
1819
import weakref
1920
from test import support
2021
from test.support import MISSING_C_DOCSTRINGS
@@ -644,6 +645,34 @@ def test_pytype_fromspec_with_repeated_slots(self):
644645
with self.assertRaises(SystemError):
645646
_testcapi.create_type_from_repeated_slots(variant)
646647

648+
def test_immutable_type_with_mutable_base(self):
649+
# Add deprecation warning here so it's removed in 3.14
650+
warnings._deprecated(
651+
'creating immutable classes with mutable bases', remove=(3, 14))
652+
653+
class MutableBase:
654+
def meth(self):
655+
return 'original'
656+
657+
with self.assertWarns(DeprecationWarning):
658+
ImmutableSubclass = _testcapi.make_immutable_type_with_base(
659+
MutableBase)
660+
instance = ImmutableSubclass()
661+
662+
self.assertEqual(instance.meth(), 'original')
663+
664+
# Cannot override the static type's method
665+
with self.assertRaisesRegex(
666+
TypeError,
667+
"cannot set 'meth' attribute of immutable type"):
668+
ImmutableSubclass.meth = lambda self: 'overridden'
669+
self.assertEqual(instance.meth(), 'original')
670+
671+
# Can change the method on the mutable base
672+
MutableBase.meth = lambda self: 'changed'
673+
self.assertEqual(instance.meth(), 'changed')
674+
675+
647676
def test_pynumber_tobase(self):
648677
from _testcapi import pynumber_tobase
649678
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:data:`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 = (int)((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
@@ -3676,6 +3676,32 @@ PyType_FromMetaclass(PyTypeObject *metaclass, PyObject *module,
36763676
goto finally;
36773677
}
36783678

3679+
/* If this is an immutable type, check if all bases are also immutable,
3680+
* and (for now) fire a deprecation warning if not.
3681+
* (This isn't necessary for static types: those can't have heap bases,
3682+
* and only heap types can be mutable.)
3683+
*/
3684+
if (spec->flags & Py_TPFLAGS_IMMUTABLETYPE) {
3685+
for (int i=0; i<PyTuple_GET_SIZE(bases); i++) {
3686+
PyTypeObject *b = (PyTypeObject*)PyTuple_GET_ITEM(bases, i);
3687+
if (!b) {
3688+
goto finally;
3689+
}
3690+
if (!_PyType_HasFeature(b, Py_TPFLAGS_IMMUTABLETYPE)) {
3691+
if (PyErr_WarnFormat(
3692+
PyExc_DeprecationWarning,
3693+
0,
3694+
"Creating immutable type %s from mutable base %s is "
3695+
"deprecated, and slated to be disallowed in Python 3.14.",
3696+
spec->name,
3697+
b->tp_name))
3698+
{
3699+
goto finally;
3700+
}
3701+
}
3702+
}
3703+
}
3704+
36793705
/* Calculate the metaclass */
36803706

36813707
if (!metaclass) {

0 commit comments

Comments
 (0)