diff --git a/peps/pep-0800.rst b/peps/pep-0800.rst index 8c2470c97cb..58731e6be61 100644 --- a/peps/pep-0800.rst +++ b/peps/pep-0800.rst @@ -2,13 +2,14 @@ PEP: 800 Title: Disjoint bases in the type system Author: Jelle Zijlstra Discussions-To: https://discuss.python.org/t/99910/ -Status: Draft +Status: Accepted Type: Standards Track Topic: Typing Created: 21-Jul-2025 Python-Version: 3.15 Post-History: `18-Jul-2025 `__, `23-Jul-2025 `__, +Resolution: `15-Apr-2026 `__ Abstract @@ -19,6 +20,10 @@ However, the information necessary to determine this is not currently part of th decorator, ``@typing.disjoint_base``, that indicates that a class is a "disjoint base". Two classes that have distinct, unrelated disjoint bases cannot have a common child class. +This decorator is not expected to be used directly by most users. It is primarily intended for use in stub files for +standard library and extension-module classes, where it helps type checkers reflect runtime restrictions +consistently. + Motivation ========== @@ -63,11 +68,11 @@ incorrect in general, as discussed in more detail :ref:`below `: it recognizes certain classes as "solid bases" that restrict multiple inheritance. Broadly speaking, every class must 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 -precise definition below. However, ty's approach relies on hardcoded knowledge of particular built-in types. The term "solid base" derives from the +precise definition below. However, ty's current approach relies on hardcoded knowledge of particular built-in types. The term "solid base" derives from the CPython implementation; this PEP uses the newly proposed term "disjoint base" instead. This PEP proposes an extension to the type system that makes it possible to express when multiple inheritance is not -allowed at runtime: an ``@disjoint_base`` decorator that marks a classes as a *disjoint base*. +allowed at runtime: an ``@disjoint_base`` decorator that marks a class as a *disjoint base*. This gives type checkers a more precise understanding of reachability, and helps in several concrete areas. Invalid class definitions @@ -256,7 +261,7 @@ Similarly, the concept of a "disjoint base" is not meaningful on ``TypedDict`` d Although they receive some special treatment in the type system, ``NamedTuple`` definitions create real nominal classes that can have child classes, so it makes sense to allow ``@disjoint_base`` on them and treat them like regular classes for the purposes of the disjoint base mechanism. All ``NamedTuple`` classes have ``tuple``, a disjoint base, in their MRO, so they -cannot multiple inherit from other disjoint bases. +cannot use multiple inheritance with other disjoint bases. Specification ============= @@ -363,8 +368,9 @@ None known. How to Teach This ================= -Most users will not have to directly use or understand the ``@disjoint_base`` decorator, as the expectation is that will be -primarily used in library stubs for low-level libraries. Teachers of Python can introduce +Most users will not have to directly use or understand the ``@disjoint_base`` decorator, as the expectation is that it will be +primarily used in library stubs for low-level libraries. It should not be taught as a decorator that users should routinely +add to classes. Teachers of Python can introduce the concept of "disjoint bases" to explain why multiple inheritance is not allowed in certain cases. Teachers of Python typing can introduce the decorator when teaching type narrowing constructs like ``isinstance()`` to explain to users why type checkers treat certain branches as unreachable. @@ -377,7 +383,8 @@ The runtime implementation of the ``@disjoint_base`` decorator is available in Mypy and its stubtest tool support the decorator as of version 1.18.1; this was implemented in `python/mypy#19678 `__. Support was added to the ty type checker in -`astral-sh/ruff#20084 `__. +`astral-sh/ruff#20084 `__ +and to pycroscope in `JelleZijlstra/pycroscope#431 `__. Appendix ======== @@ -465,9 +472,9 @@ Nevertheless, it accepts the following class definition without error:: def __rmul__(self, other: object) -> Never: raise TypeError def __ge__(self, other: int | str) -> bool: - return int(self) > other if isinstance(other, int) else str(self) > other - def __gt__(self, other: int | str) -> bool: return int(self) >= other if isinstance(other, int) else str(self) >= other + def __gt__(self, other: int | str) -> bool: + return int(self) > other if isinstance(other, int) else str(self) > other def __lt__(self, other: int | str) -> bool: return int(self) < other if isinstance(other, int) else str(self) < other def __le__(self, other: int | str) -> bool: @@ -519,7 +526,7 @@ can also reject classes that have more practically useful implementations:: pass Mypy's rule works reasonably well in practice for deducing whether an intersection of two -classes is inhabited. Most builtin classes that are disjoint bases happen to implement common dunder +classes is inhabited. Most built-in classes that are disjoint bases happen to implement common dunder methods such as ``__add__`` and ``__iter__`` in incompatible ways, so mypy will consider them incompatible. There are some exceptions: mypy allows ``class C(BaseException, int): ...``, though both of these classes are disjoint bases and the class definition is rejected at runtime.