Order-dependent overload resolution failure due to memorised Protocol conformance check on generic class
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 15.6k
- Forks
- 1.8k
- Avg merge
- 12h 13m
- Merged PRs (30d)
- 52
Description
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 here
If 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the standalone Python reproduction in the issue or its pyright-playground link, then trace overload resolution and the Protocol conformance result used for Series[A] + B and Series[bool] + True. Done means the earlier failed check does not affect the later valid specialization, and the final assert_type reports Series[bool] without an error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100