Skip to content
← All Python articles

Python explainer

Why Some Classes Cannot Share a Subclass

About 4 min to read. Allow extra time to work through examples.

Optional. See why mypy calls isinstance(value, str) unreachable when value is an int, which classes Python cannot combine, and what the @disjoint_base decorator from Python 3.15 does.

This article uses Python. You can make it your practice mode to browse related content across the site.

This article is optional, because it explains a mypy error that most code never triggers and a Python 3.15 decorator meant for library authors. You can skip it on a first pass.

Here is a check that can never succeed:

def describe(value: int) -> str:
    if isinstance(value, str):
        return "text"
    return "number"

With --warn-unreachable, which this site's grader uses, mypy reports:

error: Subclass of "int" and "str" cannot exist: have distinct disjoint bases  [unreachable]
error: Statement is unreachable  [unreachable]

Try the same check with two classes of your own, and mypy reports nothing:

class Reader:
    pass


class Writer:
    pass


def describe(value: Reader) -> str:
    if isinstance(value, Writer):
        return "reader and writer"
    return "reader"

Some class could inherit from both, such as class Duplex(Reader, Writer), and an instance of it would pass the check. So this branch can run. For int and str, no such class can exist.

Some classes have a layout of their own

Python refuses to build a class that inherits from both int and str:

try:

    class Both(int, str):
        pass

except TypeError as error:
    print(error)  # multiple bases have instance lay-out conflict

An int and a str each store their data inside the object, in a layout of their own, and one object cannot have two layouts. A class like this is a disjoint base. A class can inherit from two disjoint bases only when one of them is a subclass of the other. mypy knows which built-in classes are disjoint bases, so it knows the isinstance check at the top of this article is never true.

Non-empty slots make your own class a disjoint base

A class with non-empty __slots__ stores its attributes at fixed places inside each instance, so it has a layout of its own too. That includes a dataclass with slots=True and at least one field:

from dataclasses import dataclass


@dataclass(slots=True)
class HasX:
    x: int


@dataclass(slots=True)
class HasY:
    y: int


try:

    class Point(HasX, HasY):
        pass

except TypeError as error:
    print(error)  # multiple bases have instance lay-out conflict

mypy reports Class "Point" has incompatible disjoint bases without running the code. A class with empty slots, __slots__ = (), adds no storage, so it combines with other classes without a layout conflict.

Check your understanding Does @dataclass(slots=True) always make a class a disjoint base? Show answer

No. It does only when the dataclass adds at least one field of its own, because only then does it add a slot. A dataclass with no fields gets __slots__ = (), which adds no storage, so it combines with other classes like an empty-slots mixin.

Mark C classes with @disjoint_base

mypy sees __slots__ in Python code, but it cannot see the layout of a class written in C, such as one from an extension module. For those, the class's stub marks it with @disjoint_base, from typing in Python 3.15 or from typing_extensions in earlier versions. The stubs that ship with mypy already mark int, str and many other built-in classes this way.

The decorator only sets __disjoint_base__ = True on the class. Python itself ignores that attribute, so this code runs, and only mypy objects:

from typing import disjoint_base


@disjoint_base
class FileHandle:
    pass


@disjoint_base
class SocketHandle:
    pass


class Both(FileHandle, SocketHandle):
    pass

mypy reports Class "Both" has incompatible disjoint bases. In application code you rarely need the decorator.

Common mistakes

Deleting a check that mypy calls unreachable. The annotation value: int is a promise from the caller. If the value comes from outside your program, such as parsed JSON, it may be a string after all. Type such input as object and narrow it with isinstance, so mypy and the runtime agree. Delete the check only when the value comes from code that mypy checks.

Putting @disjoint_base on a protocol. A protocol describes methods, not a layout, so mypy rejects it with @disjoint_base cannot be used with protocol class.

Going further

PEP 800 defines disjoint bases. mypy 2.4.0, the version this site runs, reports every error shown here.

step 26 of 26

Interfaces with Protocols and ABCs

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks