https://github.com/python/cpython/commit/16185e9fe2037d2171626f79c3d099bd7772b53e
commit: 16185e9fe2037d2171626f79c3d099bd7772b53e
branch: main
author: Petr Viktorin <[email protected]>
committer: encukou <[email protected]>
date: 2026-06-17T15:27:02+02:00
summary:
gh-149044: Improve Py_tp_base[s] docs & error message for non-type bases
(GH-151252)
The initial implementation of PEP 820 worsened the error message
when non-types are given as base types in Py_tp_bases & Py_tp_base.
Bring back the 'bases must be types' wording and add a 'got' note for
easier debugging.
Improve slot ID documentation, and soft-deprecate Py_tp_base
(as per the PEP).
files:
A Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst
M Doc/c-api/type.rst
M Doc/c-api/typeobj.rst
M Doc/tools/removed-ids.txt
M Doc/whatsnew/3.15.rst
M Lib/test/test_capi/test_misc.py
M Lib/test/test_capi/test_slots.py
M Modules/_testlimitedcapi/slots.c
M Objects/typeobject.c
diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst
index 4771d0a7781bd60..48eb16bd90834ba 100644
--- a/Doc/c-api/type.rst
+++ b/Doc/c-api/type.rst
@@ -563,10 +563,10 @@ but need extra remarks for use as slots:
:c:member:`Slot ID <PySlot.sl_id>` for the name of the type,
used to set :c:member:`PyTypeObject.tp_name`.
- This slot (or :c:func:`PyType_Spec.name`) is required to create a type.
+ This slot (or :c:member:`PyType_Spec.name`) is required to create a type.
This may not be used in :c:member:`PyType_Spec.slots`.
- Use :c:func:`PyType_Spec.name` instead.
+ Use :c:member:`PyType_Spec.name` instead.
.. impl-detail::
@@ -585,7 +585,7 @@ but need extra remarks for use as slots:
The value must be positive.
This may not be used in :c:member:`PyType_Spec.slots`.
- Use :c:func:`PyType_Spec.basicsize` instead.
+ Use :c:member:`PyType_Spec.basicsize` instead.
This slot may not be used with :c:func:`PyType_GetSlot`.
Use :c:member:`PyTypeObject.tp_basicsize` instead if needed, but be aware
@@ -616,7 +616,7 @@ but need extra remarks for use as slots:
:c:macro:`!Py_tp_extra_basicsize` is an error.
This may not be used in :c:member:`PyType_Spec.slots`.
- Use negative :c:func:`PyType_Spec.basicsize` instead.
+ Use negative :c:member:`PyType_Spec.basicsize` instead.
This slot may not be used with :c:func:`PyType_GetSlot`.
@@ -648,7 +648,7 @@ but need extra remarks for use as slots:
- With the :c:macro:`Py_TPFLAGS_ITEMS_AT_END` flag.
This may not be used in :c:member:`PyType_Spec.slots`.
- Use :c:func:`PyType_Spec.itemsize` instead.
+ Use :c:member:`PyType_Spec.itemsize` instead.
This slot may not be used with :c:func:`PyType_GetSlot`.
@@ -663,13 +663,44 @@ but need extra remarks for use as slots:
:c:func:`PyType_FromSpecWithBases` sets it automatically.
This may not be used in :c:member:`PyType_Spec.slots`.
- Use negative :c:func:`PyType_Spec.basicsize` instead.
+ Use negative :c:member:`PyType_Spec.basicsize` instead.
This slot may not be used with :c:func:`PyType_GetSlot`.
Use :c:func:`PyType_GetFlags` instead.
.. versionadded:: 3.15
+.. c:macro:: Py_tp_bases
+
+ :c:member:`Slot ID <PySlot.sl_id>` for type flags, used to set
+ :c:member:`PyTypeObject.tp_bases`.
+
+ The slot can be set to a tuple of type objects which the newly created
+ type should inherit from, like the "positional arguments" of
+ a Python :ref:`class definition <class>`.
+
+ Alternately, the slot can be set to a single type object to specify
+ a single base.
+ The effect is the same as specifying a one-element tuple.
+
+ .. versionchanged:: 3.15
+
+ Previously, :c:macro:`!Py_tp_bases` required a tuple of types.
+
+.. c:macro:: Py_tp_base
+
+ Equivalent to :c:macro:`Py_tp_bases` (with ``s`` at the end).
+ If both are specified, :c:macro:`!Py_tp_bases` takes priority and
+ this slot is ignored.
+
+ .. versionchanged:: 3.15
+
+ Previously, :c:macro:`!Py_tp_base` required a single type, not a tuple.
+
+ .. soft-deprecated:: 3.15
+
+ When not targetting older Python versions, pefer :c:macro:`!Py_tp_bases`.
+
The following slots do not correspond to public fields in the
underlying structures:
diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst
index dcc9e243c2f3147..16dcb880712d244 100644
--- a/Doc/c-api/typeobj.rst
+++ b/Doc/c-api/typeobj.rst
@@ -1936,12 +1936,12 @@ and :c:data:`PyType_Type` effectively act as defaults.)
.. c:member:: PyTypeObject* PyTypeObject.tp_base
- .. corresponding-type-slot:: Py_tp_base
-
An optional pointer to a base type from which type properties are
inherited. At
this level, only single inheritance is supported; multiple inheritance
require
dynamically creating a type object by calling the metatype.
+ For the corresponding slot ID, see :c:macro:`Py_tp_base`.
+
.. note::
.. from Modules/xxmodule.c
@@ -2253,17 +2253,12 @@ and :c:data:`PyType_Type` effectively act as defaults.)
.. c:member:: PyObject* PyTypeObject.tp_bases
- .. corresponding-type-slot:: Py_tp_bases
-
Tuple of base types.
This field should be set to ``NULL`` and treated as read-only.
Python will fill it in when the type is :c:func:`initialized
<PyType_Ready>`.
- For dynamically created classes, the :c:data:`Py_tp_bases`
- :c:type:`slot <PyType_Slot>` can be used instead of the *bases* argument
- of :c:func:`PyType_FromSpecWithBases`.
- The argument form is preferred.
+ For the corresponding slot ID, see :c:macro:`Py_tp_bases`.
.. warning::
diff --git a/Doc/tools/removed-ids.txt b/Doc/tools/removed-ids.txt
index 05fc89d9fe2d278..81c0f098e4c8a83 100644
--- a/Doc/tools/removed-ids.txt
+++ b/Doc/tools/removed-ids.txt
@@ -29,3 +29,7 @@ reference/expressions.html:
grammar-token-python-grammar-enclosure
reference/expressions.html: grammar-token-python-grammar-list_display
reference/expressions.html: grammar-token-python-grammar-parenth_form
reference/expressions.html: grammar-token-python-grammar-set_display
+
+# Moved to a different page
+c-api/typeobj.html: c.Py_tp_base
+c-api/typeobj.html: c.Py_tp_bases
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 7f9a0f0e286645d..db6903da9f63444 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -2476,6 +2476,12 @@ New features
* :c:func:`PyModule_FromDefAndSpec2`
* :c:func:`PyModule_ExecDef`
+
+ The slots :c:macro:`Py_tp_bases` and :c:macro:`Py_tp_base` are now
+ equivalent: they can be set either to a single type or a tuple of types.
+ The :c:macro:`Py_tp_bases` slot is preferred; the other is ignored if both
+ are specified.
+
(Contributed by Petr Viktorin in :gh:`149044`.)
* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py
index 3debc6369e89fb4..6d84f0b8c305dfb 100644
--- a/Lib/test/test_capi/test_misc.py
+++ b/Lib/test/test_capi/test_misc.py
@@ -924,7 +924,7 @@ def test_tp_bases_slot(self):
def test_tp_bases_slot_none(self):
self.assertRaisesRegex(
TypeError,
- "metaclass conflict",
+ "bases must be types",
_testcapi.create_heapctype_with_none_bases_slot
)
diff --git a/Lib/test/test_capi/test_slots.py b/Lib/test/test_capi/test_slots.py
index c78b118712b11d5..b8b6d00b5f84d55 100644
--- a/Lib/test/test_capi/test_slots.py
+++ b/Lib/test/test_capi/test_slots.py
@@ -312,3 +312,38 @@ def test_repeat_error(self):
_testlimitedcapi.module_from_slots("repeat_exec", FakeSpec())
with self.assertRaisesRegex(SystemError, "multiple"):
_testlimitedcapi.module_from_slots("repeat_gil", FakeSpec())
+
+ def test_bases_slots(self):
+ create = _testlimitedcapi.type_from_base_slots
+
+ # Py_tp_bases overrides Py_tp_base
+ cls = create(base=int, bases=float)
+ self.assertEqual(cls.mro(), [cls, float, object])
+
+ # type is equivalent to one-element tuple
+ cls = create(base=None, bases=int)
+ self.assertEqual(cls.mro(), [cls, int, object])
+
+ cls = create(base=None, bases=(int,))
+ self.assertEqual(cls.mro(), [cls, int, object])
+
+ cls = create(base=int)
+ self.assertEqual(cls.mro(), [cls, int, object])
+
+ cls = create(base=(int,))
+ self.assertEqual(cls.mro(), [cls, int, object])
+
+ # Tuple of bases works
+ class Custom:
+ pass
+ cls = create(bases=int)
+ sub = create(base=float, bases=(Custom, cls, int))
+ self.assertEqual(sub.mro(), [sub, Custom, cls, int, object])
+
+ # Reasonable error message for non-types
+ with self.assertRaisesRegex(TypeError,
+ "bases must be types; got 'NoneType'"):
+ create(base=None)
+ with self.assertRaisesRegex(TypeError,
+ "bases must be types; got 'str'"):
+ create(bases="a string")
diff --git
a/Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst
b/Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst
new file mode 100644
index 000000000000000..fe0730b1bf87c4d
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst
@@ -0,0 +1,3 @@
+Improved error message when specifying non-type base classes in
+:c:macro:`Py_tp_bases`, :c:macro:`Py_tp_base`, and *bases* argument to
+:c:func:`PyType_FromMetaclass` and other ``PyType_From*`` functions.
diff --git a/Modules/_testlimitedcapi/slots.c b/Modules/_testlimitedcapi/slots.c
index 7a8d6466e53a096..9abe53d21154645 100644
--- a/Modules/_testlimitedcapi/slots.c
+++ b/Modules/_testlimitedcapi/slots.c
@@ -607,6 +607,47 @@ module_from_null_slot(PyObject* Py_UNUSED(module),
PyObject *args)
}, spec);
}
+
+
+static PyObject *
+type_from_base_slots(
+ PyObject *self, PyObject *args, PyObject *kwargs)
+{
+ PyObject *base = NULL;
+ PyObject *bases = NULL;
+ if (!PyArg_ParseTupleAndKeywords(
+ args, kwargs, "|OO",
+ (char*[]){"base", "bases", NULL},
+ &base, &bases))
+ {
+ return NULL;
+ }
+
+ PySlot empty_slots[] = {
+ PySlot_END
+ };
+
+ PySlot base_slots[] = {
+ PySlot_DATA(Py_tp_base, base),
+ PySlot_END
+ };
+
+ PySlot bases_slots[] = {
+ PySlot_DATA(Py_tp_bases, bases),
+ PySlot_END
+ };
+
+ PySlot slots[] = {
+ PySlot_STATIC_DATA(Py_tp_name, "_testcapi.HeapCTypeWithBases"),
+ PySlot_UINT64(Py_tp_flags, Py_TPFLAGS_BASETYPE),
+ PySlot_DATA(Py_slot_subslots, base ? base_slots: empty_slots),
+ PySlot_DATA(Py_slot_subslots, bases ? bases_slots: empty_slots),
+ PySlot_END
+ };
+
+ return PyType_FromSlots(slots);
+}
+
static PyMethodDef _TestMethods[] = {
{"type_from_slots", type_from_slots, METH_VARARGS},
{"module_from_gil_slot", module_from_gil_slot, METH_VARARGS},
@@ -614,6 +655,8 @@ static PyMethodDef _TestMethods[] = {
{"type_from_null_spec_slot", type_from_null_spec_slot, METH_VARARGS},
{"module_from_slots", module_from_slots, METH_VARARGS},
{"module_from_null_slot", module_from_null_slot, METH_VARARGS},
+ {"type_from_base_slots", _PyCFunction_CAST(type_from_base_slots),
+ METH_VARARGS | METH_KEYWORDS},
{NULL},
};
static PyMethodDef *TestMethods = _TestMethods;
diff --git a/Objects/typeobject.c b/Objects/typeobject.c
index 881ed58d275ac7a..59593fd0f6a0b78 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -3717,9 +3717,9 @@ find_best_base(PyObject *bases)
for (i = 0; i < n; i++) {
PyObject *base_proto = PyTuple_GET_ITEM(bases, i);
if (!PyType_Check(base_proto)) {
- PyErr_SetString(
+ PyErr_Format(
PyExc_TypeError,
- "bases must be types");
+ "bases must be types; got '%T'", base_proto);
return NULL;
}
PyTypeObject *base_i = (PyTypeObject *)base_proto;
@@ -4167,8 +4167,9 @@ _PyType_CalculateMetaclass(PyTypeObject *metatype,
PyObject *bases)
for (i = 0; i < nbases; i++) {
tmp = PyTuple_GET_ITEM(bases, i);
tmptype = Py_TYPE(tmp);
- if (PyType_IsSubtype(winner, tmptype))
+ if (PyType_IsSubtype(winner, tmptype)) {
continue;
+ }
if (PyType_IsSubtype(tmptype, winner)) {
winner = tmptype;
continue;
@@ -5529,6 +5530,12 @@ type_from_slots_or_spec(
}
}
+ /* Calculate best base, and check that all bases are type objects */
+ PyTypeObject *base = find_best_base(bases); // borrowed ref
+ if (base == NULL) {
+ goto finally;
+ }
+
/* Calculate the metaclass */
if (!metaclass) {
@@ -5551,11 +5558,6 @@ type_from_slots_or_spec(
goto finally;
}
- /* Calculate best base, and check that all bases are type objects */
- PyTypeObject *base = find_best_base(bases); // borrowed ref
- if (base == NULL) {
- goto finally;
- }
// find_best_base() should check Py_TPFLAGS_BASETYPE & raise a proper
// exception, here we just check its work
assert(_PyType_HasFeature(base, Py_TPFLAGS_BASETYPE));
_______________________________________________
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]