Skip to content

Fix infinite recursion in protocol subtype check (GH#21739)#21750

Open
mangodxd wants to merge 1 commit into
python:masterfrom
mangodxd:fix/protocol-infinite-loop
Open

Fix infinite recursion in protocol subtype check (GH#21739)#21750
mangodxd wants to merge 1 commit into
python:masterfrom
mangodxd:fix/protocol-infinite-loop

Conversation

@mangodxd

Copy link
Copy Markdown

Description

Fixes #21739

Problem

When a generic class references itself in a method's return type via a protocol, mypy enters an infinite loop between is_callable_compatible and is_protocol_implementation.

The reproducer:

class PExp(Protocol):
    def EQ(self, other: Any) -> "PExp": ...

class PRef(Protocol):
    def EQ(self, other: Any) -> "PExp": ...

class Exp:
    def EQ(self, other: Any) -> "Exp":
        return Exp(self, "EQ", other)

class Ref:
    get: Callable[[], Any]
    def EQ(self, other: Any) -> "Exp":
        return Exp(self, "EQ", other)

class Cache:
    def spam(self, name: str) -> "PRef":
        return Ref(lambda: "spam")

Root Cause

The assuming mechanism in is_protocol_implementation tracks (left, right) pairs to detect recursive protocol checks. When it encounters the same pair again, it returns True (assuming compatibility).

However, in this case the left type keeps growing: Ref[T]Exp[Ref[T], T]Exp[Exp[Ref[T], T], Any] → ... The right protocol stays the same, but the exact-match check on l == left never triggers because each left is a different Instance type.

Fix

Add a guard to is_protocol_implementation: if the assuming stack exceeds 50 entries (meaning we're deep in recursive protocol checks for the same protocol), bail out assuming compatibility. This prevents infinite recursion while preserving correct behavior for all normal cases.

Tests

Added testProtocolInfiniteRecursionGrowingLeft in check-protocols.test using a simplified version of the reproducer from the issue.

When checking if a class implements a protocol, the 'assuming' mechanism
tracks (left, right) pairs to detect cycles. But when the left type keeps
growing (e.g. Ref[T] -> Exp[Ref[T], T] -> Exp[Exp[Ref[T], T], Any] -> ...),
the exact-match check never triggers.

Add a guard: if the assuming stack exceeds 50 entries for the same
protocol, bail out assuming compatibility. This prevents infinite
recursion while preserving correct behavior for all normal cases.

Closes python#21739
@github-actions

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Endless call cycle between is_callable_compatible and is_protocol_implementation

1 participant