python/cpython · error · RuntimeError

Refusing to create an inheritance cycle

Error message

Refusing to create an inheritance cycle

What it means

Raised by ABCMeta.register when the requested virtual-subclass relationship would create an inheritance cycle — specifically when the ABC itself is already a (real or virtual) subclass of the class you are trying to register. Because virtual subclass checks walk cls.__subclasses__()-style registries, a cycle would break __subclasshook__ resolution, so register() refuses with RuntimeError. Note X.register(X) is explicitly a no-op, and real subclasses are accepted silently.

Source

Thrown at Lib/_py_abc.py:67

        cls._abc_cache = WeakSet()
        cls._abc_negative_cache = WeakSet()
        cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
        return cls

    def register(cls, subclass):
        """Register a virtual subclass of an ABC.

        Returns the subclass, to allow usage as a class decorator.
        """
        if not isinstance(subclass, type):
            raise TypeError("Can only register classes")
        if issubclass(subclass, cls):
            return subclass  # Already a subclass
        # Subtle: test for cycles *after* testing for "already a subclass";
        # this means we allow X.register(X) and interpret it as a no-op.
        if issubclass(cls, subclass):
            # This would create a cycle, which is bad for the algorithm below
            raise RuntimeError("Refusing to create an inheritance cycle")
        cls._abc_registry.add(subclass)
        ABCMeta._abc_invalidation_counter += 1  # Invalidate negative cache
        return subclass

    def _dump_registry(cls, file=None):
        """Debug helper to print the ABC registry."""
        print(f"Class: {cls.__module__}.{cls.__qualname__}", file=file)
        print(f"Inv. counter: {get_cache_token()}", file=file)
        for name in cls.__dict__:
            if name.startswith("_abc_"):
                value = getattr(cls, name)
                if isinstance(value, WeakSet):
                    value = set(value)
                print(f"{name}: {value!r}", file=file)

    def _abc_registry_clear(cls):
        """Clear the registry (for debugging or testing)."""
        cls._abc_registry.clear()

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Reverse the direction: register the derived class with the base ABC (BaseABC.register(Derived)), not the reverse
  2. If the class is already a real subclass, drop the register() call entirely — it is redundant
  3. Restructure so the 'provider' side is always the ABC and the 'implementer' side is always the argument
  4. Guard with issubclass checks before registering (see defense below)

Example fix

# before
class Serializer(ABC): ...
class JSONSerializer(Serializer): ...
JSONSerializer.register(Serializer)  # RuntimeError: cycle

# after
# JSONSerializer is already a real subclass; no registration needed.
# For an unrelated class, register it with the ABC:
Serializer.register(ThirdPartySerializer)
Defensive patterns

Strategy: validation

Validate before calling

def register_no_cycle(abc, candidate):
    if not isinstance(candidate, type):
        raise TypeError('Can only register classes')
    if issubclass(abc, candidate) and abc is not candidate:
        raise ValueError(f'{candidate.__name__} is a base of {abc.__name__}; registration would cycle')
    return abc.register(candidate)

Type guard

def would_cycle(abc, candidate) -> bool:
    return isinstance(candidate, type) and issubclass(abc, candidate) and abc is not candidate

Try / catch

try:
    Base.register(Other)
except RuntimeError as e:
    if 'inheritance cycle' in str(e):
        # direction is inverted; either drop it or reverse
        if issubclass(Base, Other):
            pass  # already real subclass, registration unnecessary
        else:
            raise
    else:
        raise

Prevention

When it happens

Trigger: Calling Sub.register(Super) where Sub actually inherits from Super (so issubclass(Sub, Super) is true) — e.g. class Serializable(ABC); class JSONSerializable(Serializable); then Serializable is fine, but JSONSerializable.register(Serializable) raises. Also virtual cycles: A.register(B) followed by B.register(A).

Common situations: Registration direction reversed by mistake in plugin systems ('register the base with the concrete class' instead of the opposite); decorator-based registration applied to a base class from a derived one; iterative refactoring where a base class moves under the ABC after registrations were written.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/7ce2d17476d65874. Report an issue: GitHub.