From 26f93636ad92481a4175435c01452c7b531c347d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Aug 2025 19:13:54 -0700 Subject: [PATCH 1/3] PEP 800: Rename to "disjoint base" --- peps/pep-0800.rst | 179 +++++++++++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 83 deletions(-) diff --git a/peps/pep-0800.rst b/peps/pep-0800.rst index 272d1671eae..9329d62003b 100644 --- a/peps/pep-0800.rst +++ b/peps/pep-0800.rst @@ -1,5 +1,5 @@ PEP: 800 -Title: Solid bases in the type system +Title: Disjoint bases in the type system Author: Jelle Zijlstra Discussions-To: https://discuss.python.org/t/99910/ Status: Draft @@ -15,8 +15,8 @@ Abstract To analyze Python programs precisely, type checkers need to know when two classes can and cannot have a common child class. However, the information necessary to determine this is not currently part of the type system. This PEP adds a new -decorator, ``@typing.solid_base``, that indicates that a class is a "solid base". Two classes that have distinct, unrelated -solid bases cannot have a common child class. +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. Motivation ========== @@ -65,17 +65,18 @@ inherit from at most one unique solid base, and if there is no unique solid base precise definition below. However, ty's approach relies on hardcoded knowledge of particular built-in types. This PEP proposes an extension to the type system that makes it possible to express when multiple inheritance is not -allowed at runtime: an ``@solid_base`` decorator that marks classes as "solid bases". +allowed at runtime: an ``@disjoint_base`` decorator that marks a classes as a *disjoint base*, the term we introduce +in preference to the term "solid base". This gives type checkers a more precise understanding of reachability, and helps in several concrete areas. Invalid class definitions ------------------------- -The following class definition raises an error at runtime, because ``int`` and ``str`` are distinct solid bases:: +The following class definition raises an error at runtime, because ``int`` and ``str`` are distinct disjoint bases:: class C(int, str): pass -Without knowledge of solid bases, type checkers are not currently able to detect the reason why this class +Without knowledge of disjoint bases, type checkers are not currently able to detect the reason why this class definition is invalid, though they may detect that if this class were to exist, some of its methods would be incompatible. (When it sees this class definition, mypy will point at incompatible definitions of ``__add__`` and several other methods.) @@ -88,7 +89,7 @@ Reachability We already mentioned the reachability of code using ``isinstance()``. Similar issues arise with other type narrowing constructs such as ``match`` statements: correct inference of reachability requires an understanding of -solid bases. +disjoint bases. :: @@ -125,7 +126,7 @@ do not. For example, the following set of overloads could be exploited to If a class exists that inherits from both ``A`` and ``B``, then type checkers could pick the wrong overload on a call to ``f()``. -Type checkers could detect this source of unsafety and warn about it, but a correct implementation requires an understanding of solid bases, +Type checkers could detect this source of unsafety and warn about it, but a correct implementation requires an understanding of disjoint bases, because it relies on knowing whether values that are instances of both ``A`` and ``B`` can exist. Although many type checkers already perform a version of this check for overlapping overloads, the typing specification does not currently prescribe how this check should work. This PEP does not propose to change that, but it helps provide a building block for @@ -147,55 +148,68 @@ given an intersection type ``A & B``, a type ``C`` is assignable to it if ``C`` ``A`` and ``B``, and overlaps with all of ``A`` and ``B``. ("Overlaps" here means that at least one runtime value could exist that would be a member of both types. That is, ``A`` and ``B`` overlap if ``A & B`` is inhabited.) The second part of the rule ensures that ``str`` is not assignable to a type like ``int & Any``: while ``str`` is assignable to ``Any``, it does not overlap with ``int``. But of course, we can only know that ``str`` and ``int`` do not overlap if we know -that both classes are solid bases. +that both classes are disjoint bases. Overview -------- -Solid bases can be helpful in many corners of the type system. Though some of these corners are underspecified, -speculative, or of marginal importance, in each case the concept of solid bases enables type checkers to gain a more -precise understanding than the current type system allows. Thus, solid bases provide a firm foundation +Disjoint bases can be helpful in many corners of the type system. Though some of these corners are underspecified, +speculative, or of marginal importance, in each case the concept of disjoint bases enables type checkers to gain a more +precise understanding than the current type system allows. Thus, disjoint bases provide a firm foundation (a solid base, if you will) for improving the Python type system. Rationale ========= -The concept of "solid bases" enables type checkers to understand when a common child class of two classes can and cannot -exist. To communicate this concept to type checkers, we add an ``@solid_base`` decorator to the type system that marks -a class as a solid base. The semantics are roughly that a class cannot have two unrelated solid bases. +The concept of "disjoint bases" enables type checkers to understand when a common child class of two classes can and cannot +exist. To communicate this concept to type checkers, we add an ``@disjoint_base`` decorator to the type system that marks +a class as a disjoint base. The semantics are roughly that a class cannot have two unrelated disjoint bases. + +Naming +------ + +The initial version of this PEP used the name "solid base", following the terminology used in CPython's implementation. +However, this term is somewhat vague. The alternative term "disjoint base" suggests that a class with this decorator +is disjoint from other bases, which is a good first-order description of the concept. (The exact semantics are more subtle +and are described below.) Runtime restrictions on multiple inheritance -------------------------------------------- While Python generally allows multiple inheritance, the runtime imposes various restrictions, as documented in -`CPython PR 136844 `__ (hopefully soon to be merged). +`CPython `__. Two sets of restrictions, around a consistent MRO and a consistent metaclass, can already be implemented by type checkers using information available in the type system. The third restriction, around instance layout, -is the one that requires knowledge of solid bases. Classes that contain a non-empty ``__slots__`` definition -are automatically solid bases, as are many built-in classes implemented in C. +is the one that requires knowledge of disjoint bases. Classes that contain a non-empty ``__slots__`` definition +are automatically disjoint bases, as are many built-in classes implemented in C. Alternative implementations of Python, such as PyPy, tend to behave similarly to CPython but may differ in details, -such as exactly which standard library classes are solid bases. As the type system does not currently contain any +such as exactly which standard library classes are disjoint bases. As the type system does not currently contain any explicit support for alternative Python implementations, this PEP recommends that stub libraries such as typeshed -use CPython's behavior to determine when to use the ``@solid_base`` decorator. If future extensions to the type system +use CPython's behavior to determine when to use the ``@disjoint_base`` decorator. If future extensions to the type system add support for alternative implementations (for example, branching on the value of :py:data:`sys.implementation.name `), -stubs could condition the presence of the ``@solid_base`` decorator on the implementation where necessary. +stubs could condition the presence of the ``@disjoint_base`` decorator on the implementation where necessary. -``@solid_base`` in implementation files ---------------------------------------- +Similarly, the exact set of classes that are disjoint bases at runtime may change in future versions of Python. +If this were to happen, the type stubs used by type checkers could be updated to reflect this new reality. +In other words, this PEP adds the concept of disjoint bases to the type system, but it does not prescribe exactly +which classes are disjoint bases. -The most obvious use case for the ``@solid_base`` decorator will be in stub files for C libraries, such as the standard library, -for marking solid bases implemented in C. +``@disjoint_base`` in implementation files +------------------------------------------ -However, there are also use cases for marking solid bases in implementation files, where the effect would be to disallow -the existence of child classes that inherit from the decorated class and another solid base, such as a standard library class -or another user class decorated with ``@solid_base``. For example, this could allow type checkers to flag code that can only +The most obvious use case for the ``@disjoint_base`` decorator will be in stub files for C libraries, such as the standard library, +for marking disjoint bases implemented in C. + +However, there are also use cases for marking disjoint bases in implementation files, where the effect would be to disallow +the existence of child classes that inherit from the decorated class and another disjoint base, such as a standard library class +or another user class decorated with ``@disjoint_base``. For example, this could allow type checkers to flag code that can only be reachable if a class exists that inherits from both a user class and a standard library class such as ``int`` or ``str``, which may be technically possible but not practically plausible. :: - @solid_base + @disjoint_base class BaseModel: # ... General logic for model classes pass @@ -207,120 +221,120 @@ which may be technically possible but not practically plausible. def process_species(species: Species): if isinstance(species, str): # oops, forgot `.name` pass # type checker should warn about this branch being unreachable - # BaseModel and str are solid bases, so a class that inherits from both cannot exist + # BaseModel and str are disjoint bases, so a class that inherits from both cannot exist This is similar in principle to the existing ``@final`` decorator, which also acts to restrict subclassing: in stubs, it is used to mark classes that programmatically disallow subclassing, but in implementation files, it is often used to indicate that a class is not intended to be subclassed, without runtime enforcement. -``@solid_base`` on special classes ----------------------------------- +``@disjoint_base`` on special classes +------------------------------------- -The ``@solid_base`` decorator is primarily intended for nominal classes, but the type system contains some other constructs that +The ``@disjoint_base`` decorator is primarily intended for nominal classes, but the type system contains some other constructs that syntactically use class definitions, so we have to consider whether the decorator should be allowed on them as well, and if so, what it would mean. For ``Protocol`` definitions, the most consistent interpretation would be that the only classes that can implement the protocol would be classes that use nominal inheritance from the protocol, or ``@final`` classes that implement the protocol. -Other classes either have or could potentially have a solid base that is not the protocol. This is convoluted and not useful, -so we disallow ``@solid_base`` on ``Protocol`` definitions. +Other classes either have or could potentially have a disjoint base that is not the protocol. This is convoluted and not useful, +so we disallow ``@disjoint_base`` on ``Protocol`` definitions. -Similarly, the concept of a "solid base" is not meaningful on ``TypedDict`` definitions, as TypedDicts are purely structural types. +Similarly, the concept of a "disjoint base" is not meaningful on ``TypedDict`` definitions, as TypedDicts are purely structural types. 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 ``@solid_base`` on them and treat them like regular classes for the purposes -of the solid base mechanism. All ``NamedTuple`` classes have ``tuple``, a solid base, in their MRO, so they -cannot double inherit from other solid bases. +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. Specification ============= -A decorator ``@typing.solid_base`` is added to the type system. It may only be used on nominal classes, including ``NamedTuple`` +A decorator ``@typing.disjoint_base`` is added to the type system. It may only be used on nominal classes, including ``NamedTuple`` definitions; it is a type checker error to use the decorator on a function, ``TypedDict`` definition, or ``Protocol`` definition. -We define two properties on (nominal) classes: a class may or may not *be* a solid base, and every class must *have* a valid solid base. +We define two properties on (nominal) classes: a class may or may not *be* a disjoint base, and every class must *have* a valid disjoint base. -A class is a solid base if it is decorated with ``@typing.solid_base``, or if it contains a non-empty ``__slots__`` definition. +A class is a disjoint base if it is decorated with ``@typing.disjoint_base``, or if it contains a non-empty ``__slots__`` definition. This includes classes that have ``__slots__`` because of the ``@dataclass(slots=True)`` decorator or because of the use of the ``dataclass_transform`` mechanism to add slots. -The universal base class, ``object``, is also a solid base. +The universal base class, ``object``, is also a disjoint base. -To determine a class's solid base, we look at all of its base classes to determine a set of candidate solid bases. For each base -that is itself a solid base, the candidate is the base itself; otherwise, it is the base's solid base. If the candidate set contains -a single solid base, that is the class's solid base. If there are multiple candidates, but one of them is a subclass of all other candidates, -that class is the solid base. If no such candidate exists, the class does not have a valid solid base, and therefore cannot exist. +To determine a class's disjoint base, we look at all of its base classes to determine a set of candidate disjoint bases. For each base +that is itself a disjoint base, the candidate is the base itself; otherwise, it is the base's disjoint base. If the candidate set contains +a single disjoint base, that is the class's disjoint base. If there are multiple candidates, but one of them is a subclass of all other candidates, +that class is the disjoint base. If no such candidate exists, the class does not have a valid disjoint base, and therefore cannot exist. -Type checkers must check for a valid solid base when checking class definitions, and emit a diagnostic if they encounter a class -definition that lacks a valid solid base. Type checkers may also use the solid base mechanism to determine whether types are disjoint, +Type checkers must check for a valid disjoint base when checking class definitions, and emit a diagnostic if they encounter a class +definition that lacks a valid disjoint base. Type checkers may also use the disjoint base mechanism to determine whether types are disjoint, for example when checking whether a type narrowing construct like ``isinstance()`` results in an unreachable branch. Example:: - from typing import solid_base, assert_never + from typing import disjoint_base, assert_never - @solid_base - class Solid1: + @disjoint_base + class Disjoint1: pass - @solid_base - class Solid2: + @disjoint_base + class Disjoint2: pass - @solid_base - class SolidChild(Solid1): + @disjoint_base + class DisjointChild(Disjoint1): pass - class C1: # solid base is `object` + class C1: # disjoint base is `object` pass - # OK: candidate solid bases are `Solid1` and `object`, and `Solid1` is a subclass of `object`. - class C2(Solid1, C1): # solid base is `Solid1` + # OK: candidate disjoint bases are `Disjoint1` and `object`, and `Disjoint1` is a subclass of `object`. + class C2(Disjoint1, C1): # disjoint base is `Disjoint1` pass - # OK: candidate solid bases are `SolidChild` and `Solid1`, and `SolidChild` is a subclass of `Solid1`. - class C3(SolidChild, Solid1): # solid base is `SolidChild` + # OK: candidate disjoint bases are `DisjointChild` and `Disjoint1`, and `DisjointChild` is a subclass of `Disjoint1`. + class C3(DisjointChild, Disjoint1): # disjoint base is `DisjointChild` pass - # error: candidate solid bases are `Solid1` and `Solid2`, but neither is a subclass of the other - class C4(Solid1, Solid2): + # error: candidate disjoint bases are `Disjoint1` and `Disjoint2`, but neither is a subclass of the other + class C4(Disjoint1, Disjoint2): pass - def narrower(obj: Solid1) -> None: - if isinstance(obj, Solid2): - assert_never(obj) # OK: child class of `Solid1` and `Solid2` cannot exist + def narrower(obj: Disjoint1) -> None: + if isinstance(obj, Disjoint2): + assert_never(obj) # OK: child class of `Disjoint1` and `Disjoint2` cannot exist if isinstance(obj, C1): - reveal_type(obj) # Shows a non-empty type, e.g. `Solid1 & C1` + reveal_type(obj) # Shows a non-empty type, e.g. `Disjoint1 & C1` Runtime implementation ====================== -A new decorator, ``@solid_base``, will be added to the ``typing`` module. Its runtime behavior (consistent with -similar decorators like ``@final``) is to set an attribute ``.__solid_base__ = True`` on the decorated object, +A new decorator, ``@disjoint_base``, will be added to the ``typing`` module. Its runtime behavior (consistent with +similar decorators like ``@final``) is to set an attribute ``.__disjoint_base__ = True`` on the decorated object, then return its argument:: - def solid_base(cls): - cls.__solid_base__ = True + def disjoint_base(cls): + cls.__disjoint_base__ = True return cls -The ``__solid_base__`` attribute may be used for runtime introspection. However, there is no runtime +The ``__disjoint_base__`` attribute may be used for runtime introspection. However, there is no runtime enforcement of this decorator on user-defined classes. -It will be useful to validate whether the ``@solid_base`` decorator should be applied in a stub. While -CPython does not document precisely which classes are solid bases, it is possible to replicate the behavior +It will be useful to validate whether the ``@disjoint_base`` decorator should be applied in a stub. While +CPython does not document precisely which classes are disjoint bases, it is possible to replicate the behavior of the interpreter using runtime introspection (`example implementation `__). Stub validation tools, such as mypy's ``stubtest``, could use this logic to check whether the -``@solid_base`` decorator is applied to the correct classes in stubs. +``@disjoint_base`` decorator is applied to the correct classes in stubs. Backward compatibility ====================== -For compatibility with earlier versions of Python, the ``@solid_base`` decorator will be added to the +For compatibility with earlier versions of Python, the ``@disjoint_base`` decorator will be added to the ``typing_extensions`` backport package. At runtime, the new decorator poses no compatibility issues. -In stubs, the decorator may be added to solid base classes even if not all type checkers understand the decorator yet; +In stubs, the decorator may be added to disjoint base classes even if not all type checkers understand the decorator yet; such type checkers should simply treat the decorator as a no-op. When type checkers add support for this PEP, users may see some changes in type checking behavior around reachability @@ -337,9 +351,9 @@ None known. How to Teach This ================= -Most users will not have to directly use or understand the ``@solid_base`` decorator, as the expectation is that will be +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 -the concept of "solid bases" to explain why multiple inheritance is not allowed in certain cases. Teachers of +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. @@ -348,7 +362,6 @@ Reference Implementation None yet. - Appendix ======== @@ -489,16 +502,16 @@ 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 solid bases happen to implement common dunder +classes is inhabited. Most builtin 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 solid bases and the class definition is rejected at runtime. +though both of these classes are disjoint bases and the class definition is rejected at runtime. Conversely, when multiple inheritance is used in practice, usually the parent classes will not have incompatible methods. Thus, mypy's approach to deciding that two classes cannot intersect is both too broad (it incorrectly considers some intersections to be uninhabited) and too narrow (it misses -some intersections that are uninhabited because of solid bases). This is discussed in +some intersections that are uninhabited because of disjoint bases). This is discussed in `an issue on the mypy tracker `__. Copyright From ba5f5576c8d2337b14fd323f722fd6a8017052b7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Aug 2025 08:27:15 -0700 Subject: [PATCH 2/3] reword --- peps/pep-0800.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/peps/pep-0800.rst b/peps/pep-0800.rst index 9329d62003b..e2bdb47d913 100644 --- a/peps/pep-0800.rst +++ b/peps/pep-0800.rst @@ -62,11 +62,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. +precise definition below. However, ty's 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*, the term we introduce -in preference to the term "solid base". +allowed at runtime: an ``@disjoint_base`` decorator that marks a classes as a *disjoint base*. This gives type checkers a more precise understanding of reachability, and helps in several concrete areas. Invalid class definitions From 97d496c43567c4a08a209ce563110d747d1c4a33 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Aug 2025 20:30:39 -0700 Subject: [PATCH 3/3] intersphinx --- peps/conf.py | 1 + peps/pep-0800.rst | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/peps/conf.py b/peps/conf.py index d327b5a1d23..57e7bfec7c4 100644 --- a/peps/conf.py +++ b/peps/conf.py @@ -73,6 +73,7 @@ "py3.12": ("https://docs.python.org/3.12/", None), "py3.13": ("https://docs.python.org/3.13/", None), "py3.14": ("https://docs.python.org/3.14/", None), + "py3.15": ("https://docs.python.org/3.15/", None), "python": ("https://docs.python.org/3/", None), "trio": ("https://trio.readthedocs.io/en/latest/", None), "typing": ("https://typing.python.org/en/latest/", None), diff --git a/peps/pep-0800.rst b/peps/pep-0800.rst index e2bdb47d913..887f7b81d1f 100644 --- a/peps/pep-0800.rst +++ b/peps/pep-0800.rst @@ -176,8 +176,8 @@ and are described below.) Runtime restrictions on multiple inheritance -------------------------------------------- -While Python generally allows multiple inheritance, the runtime imposes various restrictions, as documented in -`CPython `__. +While Python generally allows multiple inheritance, the runtime imposes various restrictions, as +:external+py3.15:ref:`documented in CPython `. Two sets of restrictions, around a consistent MRO and a consistent metaclass, can already be implemented by type checkers using information available in the type system. The third restriction, around instance layout, is the one that requires knowledge of disjoint bases. Classes that contain a non-empty ``__slots__`` definition