Order-dependent overload resolution failure due to memorised Protocol conformance check on generic class
Related to #10607, now stand-alone example.
Describe the bug
When an operation evaluates whether a generic class matches an @overload conditioned on a Protocol parameter and fails (e.g. Series[A] does not implement Supports_ProtoAdd[B, A], see the example below), pyright appears to memorise or cache a negative conformance result for the generic class against that subsequent expressions on valid specializations (such as Series[bool] with True, where Series[bool] correctly implements Supports_ProtoAdd[bool, bool]) erroneously bypass the protocol overload and fall through to fallback overloads (e.g., matching Series[bool] + int because bool is a subclass of int), returning the wrong type.
If the prior failing operation is removed or moved after the valid operation, both expressions type-check correctly with 0 errors.
Code or Screenshots
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, assert_type, overload
T_contra = TypeVar("T_contra", contravariant=True)
T = TypeVar("T")
S = TypeVar("S")
S_contra = TypeVar("S_contra", contravariant=True)
class ElementOpsMixin(Generic[S]):
@overload
def _proto_add(self: "ElementOpsMixin[bool]", other: bool, /) -> "ElementOpsMixin[bool]": ...
@overload
def _proto_add(self: "ElementOpsMixin[int]", other: int, /) -> "ElementOpsMixin[int]": ...
def _proto_add(self, other: object, /) -> object:
return self
class Supports_ProtoAdd(Protocol[T_contra, T]):
def _proto_add(self, other: T_contra, /) -> ElementOpsMixin[T]: ...
class Series(ElementOpsMixin[S], Generic[S]):
@overload
def __add__(self: Supports_ProtoAdd[S_contra, S], other: S_contra, /) -> "Series[S]": ...
@overload
def __add__(self: "Series[bool]", other: int, /) -> "Series[int]": ...
def __add__(self, other: object, /) -> object:
return self
class A: ...
class B: ...
# Step 1: Evaluate mismatched operation on Series[A] + B
# Series[A] does NOT implement _proto_add for B.
s_a: Series[A] = Series()
b: B = B()
if TYPE_CHECKING:
_ = s_a + b # type: ignore[operator] # pyright: ignore[reportOperatorIssue,reportUnknownVariableType] # pyrefly: ignore[unsupported-operation] # ty: ignore[unsupported-operator]
# Step 2: Now evaluate valid operation Series[bool] + True
s_bool: Series[bool] = Series()
# EXPECTED: Series[bool] (via Supports_ProtoAdd[bool, bool])
# ACTUAL: Series[int] (Supports_ProtoAdd was cached as invalid, falling back to Series[bool] + int because bool is a subtype of int)
res = s_bool + True
assert_type(res, Series[bool]) # only pyright raises hereIf your code relies on symbols that are imported from a third-party library, include the associated import statements and specify which versions of those libraries you have installed.
VS code or command line
playground
Co-authored by Gemini 3.7 Flash
Source: microsoft/pyright