]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-149044: Improve Py_tp_base[s] docs & error message for non-type bases ...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 18 Jun 2026 08:17:15 +0000 (10:17 +0200)
committerGitHub <noreply@github.com>
Thu, 18 Jun 2026 08:17:15 +0000 (10:17 +0200)
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).
(cherry picked from commit 16185e9fe2037d2171626f79c3d099bd7772b53e)

Co-authored-by: Petr Viktorin <encukou@gmail.com>
Doc/c-api/type.rst
Doc/c-api/typeobj.rst
Doc/tools/removed-ids.txt
Doc/whatsnew/3.15.rst
Lib/test/test_capi/test_misc.py
Lib/test/test_capi/test_slots.py
Misc/NEWS.d/next/C_API/2026-06-10-15-22-44.gh-issue-149044.O7KEcs.rst [new file with mode: 0644]
Modules/_testlimitedcapi/slots.c
Objects/typeobject.c

index 4771d0a7781bd6030f498efc137d47c7ac060d2a..48eb16bd90834baf0cb24e7d188de044c72592ad 100644 (file)
@@ -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:
 
index dcc9e243c2f314796ebadc4dfc563cdc95e57880..16dcb880712d244be54f600b30fd4a18de0c5c63 100644 (file)
@@ -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::
 
index 474376f4bd7baed21022f8141c6233a1dd39d8c2..ffffda3506c06638c56477ff4d4efb534b3e4186 100644 (file)
@@ -21,3 +21,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
index 36a18f15a3deb2ee6b7d5620345f981d6699626f..88b238c709dd0e3757a1ebaba84395cf426b57bb 100644 (file)
@@ -2474,6 +2474,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
index 3debc6369e89fb4c5d4ff3b99885479f86392ba8..6d84f0b8c305dfb90c9aa350c481b97e2018cbcc 100644 (file)
@@ -924,7 +924,7 @@ class CAPITest(unittest.TestCase):
     def test_tp_bases_slot_none(self):
         self.assertRaisesRegex(
             TypeError,
-            "metaclass conflict",
+            "bases must be types",
             _testcapi.create_heapctype_with_none_bases_slot
         )
 
index c78b118712b11d5bc930a5a18000ee89721a5651..b8b6d00b5f84d55bbf946d415017f60d4b64dbcc 100644 (file)
@@ -312,3 +312,38 @@ class ModuleSlotsTests(unittest.TestCase):
             _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 (file)
index 0000000..fe0730b
--- /dev/null
@@ -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.
index 7a8d6466e53a0965cf0dd7f26d346670948cf0d1..9abe53d21154645b9796a4370a15df7c15ff8c6f 100644 (file)
@@ -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;
index e0464fe6475cfd2ff96a151020de809490f74c93..12821b134d970962c0a1850dbd440cca1f261b20 100644 (file)
@@ -3712,9 +3712,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;
@@ -4162,8 +4162,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;
@@ -5524,6 +5525,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) {
@@ -5546,11 +5553,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));