From a0f382aa2763f4ec94bb3968dd3a2638614b8661 Mon Sep 17 00:00:00 2001 From: "Aryan Singh K." <70511529+aryansk@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:51:53 +0530 Subject: [PATCH] Recognize Iterator as Hashable --- mypy/subtypes.py | 12 ++++++++++++ test-data/unit/check-protocols.test | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2d97dd534d9f5..5f0504ddf15a9 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1245,9 +1245,21 @@ def f(self) -> A: ... left = mypy.typeops.tuple_fallback(left) # We need to record this check to generate protocol fine-grained dependencies. type_state.record_protocol_subtype_check(left.type, right.type) + # Iterator instances inherit object.__hash__ at runtime, but Iterator is a + # protocol and therefore doesn't expose that inherited member structurally. + # Keep the special handling limited to the standard Hashable protocol: a + # concrete unhashable iterator must still be rejected. See #21813. + if is_named_instance(left, "typing.Iterator") and is_named_instance(right, "typing.Hashable"): + return True # nominal subtyping currently ignores '__init__' and '__new__' signatures members_not_to_check = {"__init__", "__new__"} members_not_to_check.update(skip) + # Iterator instances inherit object.__hash__ at runtime, but Iterator is a + # protocol and therefore doesn't expose that inherited member structurally. + # Keep the special handling limited to the standard Hashable protocol: a + # concrete unhashable iterator must still be rejected. See #21813. + if is_named_instance(left, "typing.Iterator") and is_named_instance(right, "typing.Hashable"): + members_not_to_check.add("__hash__") # Trivial check that circumvents the bug described in issue 9771: if left.type.is_protocol: members_right = set(right.type.protocol_members) - members_not_to_check diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 6e86507eb48d7..b036f70ea8676 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -1025,7 +1025,7 @@ main:15: note: def [T2] a(self, other: P1[T2]) -> Any [case testHashable] -from typing import Hashable, Iterable +from typing import Hashable, Iterable, Iterator def f(x: Hashable) -> None: pass @@ -1033,6 +1033,20 @@ def f(x: Hashable) -> None: def g(x: Iterable[str]) -> None: f(x) # E: Argument 1 to "f" has incompatible type "Iterable[str]"; expected "Hashable" +def h(x: Iterator[str]) -> None: + f(x) + +class UnhashableIterator(Iterator[str]): + __hash__ = None # type: ignore[assignment] + + def __next__(self) -> str: + return "" + +def k(x: UnhashableIterator) -> None: + f(x) # E: Argument 1 to "f" has incompatible type "UnhashableIterator"; expected "Hashable" \ + # N: Following member(s) of "UnhashableIterator" have conflicts: \ + # N: __hash__: expected "Callable[[], int]", got "None" + [builtins fixtures/object_hashable.pyi] [typing fixtures/typing-full.pyi]