From fc46afc7ef5e49af8a7cc140f3af466b3d4002cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Mon, 31 Aug 2026 16:00:05 +0100 Subject: [PATCH 1/3] Add reflected set operators to AbstractSet collections.abc.Set defines __rand__, __ror__, __rsub__, and __rxor__ alongside their forward counterparts, but the stub only had __and__, __or__, __sub__, and __xor__. Without them, a class that only implements __rsub__ (etc.) won't satisfy a Protocol expecting it, even though set and frozenset both do this at runtime, since the reflected methods just fall back to the same underlying implementation. For __rand__/__ror__/__rxor__ this is exact: CPython's _collections_abc.Set assigns them as literal aliases of __and__/__or__/__xor__ (same function object), which makes sense given intersection, union, and symmetric difference don't care which operand comes first. __rsub__ has its own separate body, though, since subtraction isn't commutative. Reading it shows the result's elements come from whatever the *other* operand contributes (the elements that survive filtering against self), not from self's own element type, so its signature takes an Iterable[_T] and returns AbstractSet[_T] rather than mirroring __sub__. Verified against the reproduction from the issue (a Protocol with just __rsub__/__rand__ failing to accept a plain set()) with both mypy and pyright, and confirmed the assignment error reappears if the fix is reverted. mypy_test.py and pyright_test.py both pass on stdlib/typing.pyi across all supported Python versions, and stubtest_stdlib.py shows no new errors for typing. --- stdlib/typing.pyi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stdlib/typing.pyi b/stdlib/typing.pyi index 213346ab5fe2..49ad052bdeff 100644 --- a/stdlib/typing.pyi +++ b/stdlib/typing.pyi @@ -739,9 +739,13 @@ class AbstractSet(Collection[_T_co]): def __gt__(self, other: AbstractSet[Any], /) -> bool: ... def __ge__(self, other: AbstractSet[Any], /) -> bool: ... def __and__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... + def __rand__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... def __or__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... + def __ror__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... def __sub__(self, other: AbstractSet[Any], /) -> AbstractSet[_T_co]: ... + def __rsub__(self, other: Iterable[_T], /) -> AbstractSet[_T]: ... def __xor__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... + def __rxor__(self, other: AbstractSet[_T], /) -> AbstractSet[_T_co | _T]: ... def __eq__(self, other: object, /) -> bool: ... def isdisjoint(self, other: Iterable[Any], /) -> bool: ... From 2dde9dd5727c2af56667ee2505cef817bcdc7cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Mon, 31 Aug 2026 16:41:16 +0100 Subject: [PATCH 2/3] Silence downstream override mismatches from AbstractSet.__rsub__ Adding __rsub__ to AbstractSet in the previous commit surfaces two real Liskov substitution violations that already existed but were invisible before: multiprocessing.managers._BaseSetProxy.__rsub__ returns a plain set instead of an AbstractSet-conformant value, and boltons' IndexedSet.__rsub__ returns a distinct _RSub protocol type that isn't a Set at all. Both subclasses were already inconsistent with what Set.__rsub__ promises, this fix just makes that visible for the first time. Silenced both the same way this file already handles other set operators with the same kind of mismatch (see __ior__, __ixor__, __ror__, __rxor__ nearby), rather than trying to reshape either class's actual runtime behavior to fit the protocol. --- stdlib/multiprocessing/managers.pyi | 2 +- stubs/boltons/boltons/setutils.pyi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/multiprocessing/managers.pyi b/stdlib/multiprocessing/managers.pyi index 0f1d4e87b0dc..9e126aeac846 100644 --- a/stdlib/multiprocessing/managers.pyi +++ b/stdlib/multiprocessing/managers.pyi @@ -194,7 +194,7 @@ if sys.version_info >= (3, 14): def __eq__(self, value: object, /) -> bool: ... def __rand__(self, value: AbstractSet[object], /) -> set[_T]: ... def __ror__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] - def __rsub__(self, value: AbstractSet[_T], /) -> set[_T]: ... + def __rsub__(self, value: AbstractSet[_T], /) -> set[_T]: ... # type: ignore[override] def __rxor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] def __class_getitem__(cls, item: Any, /) -> GenericAlias: ... diff --git a/stubs/boltons/boltons/setutils.pyi b/stubs/boltons/boltons/setutils.pyi index d94de1967fc0..92a3945bdae0 100644 --- a/stubs/boltons/boltons/setutils.pyi +++ b/stubs/boltons/boltons/setutils.pyi @@ -40,7 +40,7 @@ class IndexedSet(MutableSet[Any]): # __sub__ = difference # __xor__ = symmetric_difference __rxor__ = symmetric_difference - def __rsub__(self, other: _RSub[_T_co]) -> _RSub[_T_co]: ... + def __rsub__(self, other: _RSub[_T_co]) -> _RSub[_T_co]: ... # type: ignore[override] def update(self, *others: Iterable[Any]) -> None: ... def intersection_update(self, *others: Iterable[Any]) -> None: ... def difference_update(self, *others: Container[Any]) -> None: ... From 9c9a37679f568a1abf5b5065d9ce3ae9302ef223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Mon, 31 Aug 2026 16:52:31 +0100 Subject: [PATCH 3/3] Give set and frozenset their own reflected set operators Adding __rsub__ (and the other reflected operators) to AbstractSet meant set and frozenset now inherited it instead of relying purely on their forward methods, and mypy's subclass-priority rule for reflected binary operators started routing some calls through the inherited abstract version instead of the concrete one whenever the right operand's type was a subtype of the left operand's element type. That changed inferred types from concrete set/frozenset to plain AbstractSet in that case, which the regression test suite catches (stdlib/@tests/test_cases/builtins/check_set.py). Giving set and frozenset their own __rand__/__ror__/__rsub__/__rxor__ restores the previous concrete return types. The forward and reflected operators on these two classes are still allowed to overlap in a way mypy considers unsafe in the abstract, same as their existing __iand__/__ior__/__isub__/__ixor__ neighbors already do, so this follows that same established pattern rather than introducing a new one. --- stdlib/builtins.pyi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi index 903b13aa460a..40a8eff45073 100644 --- a/stdlib/builtins.pyi +++ b/stdlib/builtins.pyi @@ -1455,12 +1455,16 @@ class set(MutableSet[_T]): def __contains__(self, o: object, /) -> bool: ... def __iter__(self) -> Iterator[_T]: ... def __and__(self, value: AbstractSet[object], /) -> set[_T]: ... + def __rand__(self, value: AbstractSet[object], /) -> set[_T]: ... def __iand__(self, value: AbstractSet[object], /) -> Self: ... def __or__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... + def __ror__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] def __ior__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] def __sub__(self, value: AbstractSet[object], /) -> set[_T]: ... + def __rsub__(self, value: AbstractSet[_S], /) -> set[_S]: ... # type: ignore[override,misc] def __isub__(self, value: AbstractSet[object], /) -> Self: ... def __xor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... + def __rxor__(self, value: AbstractSet[_S], /) -> set[_T | _S]: ... # type: ignore[misc] def __ixor__(self, value: AbstractSet[_T], /) -> Self: ... # type: ignore[override,misc] def __le__(self, value: AbstractSet[object], /) -> bool: ... def __lt__(self, value: AbstractSet[object], /) -> bool: ... @@ -1489,9 +1493,13 @@ class frozenset(AbstractSet[_T_co]): def __contains__(self, o: object, /) -> bool: ... def __iter__(self) -> Iterator[_T_co]: ... def __and__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ... + def __rand__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ... def __or__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ... + def __ror__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ... # type: ignore[misc] def __sub__(self, value: AbstractSet[object], /) -> frozenset[_T_co]: ... + def __rsub__(self, value: AbstractSet[_S], /) -> frozenset[_S]: ... # type: ignore[override,misc] def __xor__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ... + def __rxor__(self, value: AbstractSet[_S], /) -> frozenset[_T_co | _S]: ... # type: ignore[misc] def __le__(self, value: AbstractSet[object], /) -> bool: ... def __lt__(self, value: AbstractSet[object], /) -> bool: ... def __ge__(self, value: AbstractSet[object], /) -> bool: ...