You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: peps/pep-0800.rst
+156-3Lines changed: 156 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,9 +57,9 @@ here: CPython does not allow a class to inherit from both ``int`` and ``str``, s
57
57
However, the information necessary to determine that these base classes are incompatible is not currently available in
58
58
the type system. Mypy, in fact, uses a heuristic based on the presence of incompatible methods; this heuristic works
59
59
reasonably well in practice, especially for built-in types, but it is
60
-
`incorrect in general<https://github.com/python/mypy/issues/19377>`__.
60
+
incorrect in general, as discussed in more detail :ref:`below <pep-800-mypy-incompatibility-check>`.
61
61
62
-
The experimental ``ty`` type checker uses a third approach that aligns more closely with the runtime behavior of Python:
62
+
The experimental ``ty`` type checker uses a third approach that aligns more closely with the :ref:`runtime behavior of Python<pep-800-solid-bases-cpython>`:
63
63
it recognizes certain classes as "solid bases" that restrict multiple inheritance. Broadly speaking, every class must
64
64
inherit from at most one unique solid base, and if there is no unique solid base, the class cannot exist; we'll provide a more
65
65
precise definition below. However, ty's approach relies on hardcoded knowledge of particular built-in types.
@@ -302,7 +302,8 @@ then return its argument::
302
302
cls.__solid_base__ = True
303
303
return cls
304
304
305
-
The ``__solid_base__`` attribute may be used for runtime introspection.
305
+
The ``__solid_base__`` attribute may be used for runtime introspection. However, there is no runtime
306
+
enforcement of this decorator on user-defined classes.
306
307
307
308
It will be useful to validate whether the ``@solid_base`` decorator should be applied in a stub. While
308
309
CPython does not document precisely which classes are solid bases, it is possible to replicate the behavior
@@ -348,6 +349,158 @@ Reference Implementation
348
349
None yet.
349
350
350
351
352
+
Appendix
353
+
========
354
+
355
+
This appendix discusses the existing situation around multiple inheritance in the type system and
356
+
in the CPython runtime in more detail.
357
+
358
+
.. _pep-800-solid-bases-cpython:
359
+
360
+
Solid bases in CPython
361
+
----------------------
362
+
363
+
The concept of "solid bases" has been part of the CPython implementation for a long time;
364
+
the concept dates back to `a 2001 commit <https://github.com/python/cpython/commit/6d6c1a35e08b95a83dbe47dbd9e6474daff00354>`__.
365
+
Nevertheless, the concept has received little attention in the documentation.
366
+
Although details of the mechanism are closely tied to CPython's internal object representation,
367
+
it is useful to explain at a high level how and why CPython works this way.
368
+
369
+
Every object in CPython is essentially a pointer to a C struct, a contiguous piece of memory that
370
+
contains information about the object. Some information is managed by the interpreter and shared
371
+
by many or all objects, such as a reference to the type of the object, and the attribute ``__dict__``
372
+
for user-defined objects. Some classes contain additional information that is specific to that class.
373
+
For example, user-defined classes with ``__slots__`` contain a place in memory for each slot,
374
+
and the built-in ``float`` class contains a C ``double`` value that stores the value of the float.
375
+
Code that interacts with these classes usually assumes a certain memory layout: C code that
376
+
interacts with a ``float`` expects to find the value at a particular offset in the object's memory.
377
+
378
+
When a child class is created, CPython must create a memory layout for the new class that
379
+
is compatible with all of its parent classes. For example, when a child class of ``float``
380
+
is created, it must be possible to pass instances of the child class to C code that interacts
381
+
directly with the underlying struct for the ``float`` class. Therefore, such a subclass must store
382
+
the ``double`` value at the same offset as the parent ``float`` class does. It may, however, add
383
+
additional fields at the end of the struct. CPython knows how to do this with the ``__dict__``
384
+
attribute, which is why it is possible to create a child class of ``float`` that adds a ``__dict__``.
385
+
386
+
However, there is no way to combine a ``float``, which must have a ``double`` in its struct,
387
+
with another C type like ``int``, which stores different data at the same spot. Therefore,
388
+
a common subclass of ``float`` and ``int`` cannot exist. We say that ``float`` and ``int``
389
+
are solid bases.
390
+
391
+
Classes implemented in C are solid bases if if they have an underlying struct that stores
392
+
data at a fixed offset, and that struct is different from the struct of its parent class.
393
+
C classes may also store a variable-size array of data (such as the contents of a string);
394
+
if this differs from the parent class, the class also becomes a solid base.
395
+
CPython's implementation deduces this from the :c:member:`~PyTypeObject.tp_itemsize`
396
+
and :c:member:`~PyTypeObject.tp_basicsize` fields of the type object, which are also
397
+
accessible from Python code as the undocumented attributes ``__itemsize__`` and ``__basicsize__``
398
+
on type objects.
399
+
400
+
Similarly, classes implemented in Python are solid bases if they have ``__slots__``, because
401
+
slots force a particular memory layout.
402
+
403
+
.. _pep-800-mypy-incompatibility-check:
404
+
405
+
Mypy's incompatibility check
406
+
----------------------------
407
+
408
+
The mypy type checker considers two classes to be incompatible if they have
409
+
incompatible methods. For example, mypy considers the ``int`` and ``str`` classes to be incompatible
410
+
because they have incompatible definitions of various methods. Given a class definition like::
411
+
412
+
class C(int, str):
413
+
pass
414
+
415
+
Mypy will output ``Definition of "__add__" in base class "int" is incompatible with definition in base class "str"``,
416
+
and similar errors for a number of other methods. These errors are correct, because the definitions of
417
+
``__add__`` in the two classes are indeed incompatible: ``int.__add__`` expects an ``int`` argument, while
418
+
``str.__add__`` expects a ``str``. If this class were to exist, at runtime ``__add__`` would resolve to
419
+
``int.__add__``. Instances of ``C`` would also be members of the ``str`` type, but they would not support
420
+
some of the operations that ``str`` supports, such as concatenation with another ``str``.
421
+
422
+
So far, so good. But mypy also uses very similar logic to conclude that no class
423
+
can inherit from both ``int`` and ``str``.
424
+
Nevertheless, it accepts the following class definition without error::
425
+
426
+
from typing import Never
427
+
428
+
class C(int, str):
429
+
def __add__(self, other: object) -> Never:
430
+
raise TypeError
431
+
def __mod__(self, other: object) -> Never:
432
+
raise TypeError
433
+
def __mul__(self, other: object) -> Never:
434
+
raise TypeError
435
+
def __rmul__(self, other: object) -> Never:
436
+
raise TypeError
437
+
def __ge__(self, other: int | str) -> bool:
438
+
return int(self) > other if isinstance(other, int) else str(self) > other
439
+
def __gt__(self, other: int | str) -> bool:
440
+
return int(self) >= other if isinstance(other, int) else str(self) >= other
441
+
def __lt__(self, other: int | str) -> bool:
442
+
return int(self) < other if isinstance(other, int) else str(self) < other
443
+
def __le__(self, other: int | str) -> bool:
444
+
return int(self) <= other if isinstance(other, int) else str(self) <= other
445
+
def __getnewargs__(self) -> Never:
446
+
raise TypeError
447
+
448
+
There is a similar situation with attributes. Given two classes with incompatible
449
+
attributes, mypy claims that a common subclass cannot exist, yet it accepts
450
+
a subclass that overrides these attributes to make them compatible::
451
+
452
+
from typing import Never
453
+
454
+
class X:
455
+
a: int
456
+
457
+
class Y:
458
+
a: str
459
+
460
+
class Z(X, Y):
461
+
@property
462
+
def a(self) -> Never:
463
+
raise RuntimeError("no luck")
464
+
@a.setter
465
+
def a(self, value: int | str) -> None:
466
+
pass
467
+
468
+
While the examples given so far rely on overrides that return ``Never``, mypy's rule
469
+
can also reject classes that have more practically useful implementations::
470
+
471
+
from typing import Literal
472
+
473
+
class Carnivore:
474
+
def eat(self, food: Literal["meat"]) -> None:
475
+
print("devouring meat")
476
+
477
+
class Herbivore:
478
+
def eat(self, food: Literal["plants"]) -> None:
479
+
print("nibbling on plants")
480
+
481
+
class Omnivore(Carnivore, Herbivore):
482
+
def eat(self, food: str) -> None:
483
+
print(f"eating {food}")
484
+
485
+
def is_it_both(obj: Carnivore):
486
+
# mypy --warn-unreachable:
487
+
# Subclass of "Carnivore" and "Herbivore" cannot exist: would have incompatible method signatures
488
+
if isinstance(obj, Herbivore):
489
+
pass
490
+
491
+
Mypy's rule works reasonably well in practice for deducing whether an intersection of two
492
+
classes is inhabited. Most builtin classes that are solid bases happen to implement common dunder
493
+
methods such as ``__add__`` and ``__iter__`` in incompatible ways, so mypy will consider them
494
+
incompatible. There are some exceptions: mypy allows ``class C(BaseException, int): ...``,
495
+
though both of these classes are solid bases and the class definition is rejected at runtime.
496
+
Conversely, when multiple inheritance is used in practice, usually the parent classes will not
497
+
have incompatible methods.
498
+
499
+
Thus, mypy's approach to deciding that two classes cannot intersect is both too broad
500
+
(it incorrectly considers some intersections to be uninhabited) and too narrow (it misses
501
+
some intersections that are uninhabited because of solid bases). This is discussed in
502
+
`an issue on the mypy tracker <https://github.com/python/mypy/issues/19377>`__.
0 commit comments