Repository: python/mypy (Python)

Issue:
Unpacking an iterable with an `__iter__` method that returns `self` leads to variable being silently inferred as `Any`
This is similar to #14811, but still reproduces on `master` following https://github.com/python/mypy/commit/bed49ab3781f28be2a6cf5c6665934a8fa2bb5f2:

```python
from typing import TypeVar

T = TypeVar("T")

class Foo:
    count: int
    def __init__(self) -> None:
        self.count = 0
    def __iter__(self: T) -> T:
        return self
    def __next__(self) -> int:
        self.count += 1
        if self.count > 3:
            raise StopIteration
        return self.count

a, b, c = Foo()
reveal_type(a)  # note: Revealed type is "Any"
```

Mypy should have enough information here to infer that the type of `a` is `int`.

**Mypy versions tested on**
- mypy 1.2.0+dev.bed49ab3781f28be2a6cf5c6665934a8fa2bb5f2 (compiled: no)
- Also reproducible on mypy playground on mypy 1.0.0: https://mypy-play.net/?mypy=latest&python=3.11&gist=fcc3254474f3eda979e0412589379404


Hints:
This one-line change fixes the issue:

```diff
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -3633,7 +3633,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
     ) -> None:
         rvalue_type = get_proper_type(rvalue_type)
         if self.type_is_iterable(rvalue_type) and isinstance(rvalue_type, Instance):
-            item_type = self.iterable_item_type(rvalue_type)
+            _, item_type = self.analyze_iterable_item_type_without_expression(rvalue_type, context)
```

But it causes this test to fail:

https://github.com/python/mypy/blob/bed49ab3781f28be2a6cf5c6665934a8fa2bb5f2/test-data/unit/check-inference.test#L377-L389

The test succeeds once again if we add annotations:

```diff
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -375,10 +375,10 @@ def f(d: Any) -> None:
     b.x

 [case testInferringTypesFromIterable]
-from typing import Iterable
+from typing import Iterable, Iterator
 class Nums(Iterable[int]):
-    def __iter__(self): pass
-    def __next__(self): pass
+    def __iter__(self) -> Iterator[int]: pass
+    def __next__(self) -> int: pass
```

@sobolevn, do you think this would be a reasonable fix? It makes me wonder why we have the `iterable_item_type()` method at all, and whether other uses of this method in `checker.py` should also be converted to `analyze_iterable_item_type_without_expression()`
I can't tell whether the required change to `testInferringTypesFromIterable` would be a feature or a bug. Mypy usually infers that unannotated methods return `Any`. It feels weird to me that this test case is currently asserting that mypy should infer a non-`Any` type here, despite both `__iter__` and `__next__` being unannotated.
> It makes me wonder why we have the iterable_item_type() method at all, and whether other uses of this method in checker.py should also be converted to analyze_iterable_item_type_without_expression()

I don't really know. It was my first `Iterable`-related PR.

> I can't tell whether the required change to testInferringTypesFromIterable would be a feature or a bug

It is really hard to tell whether your solution is a bug or an improvement (sadly, python's type system is not documented enough).
Thanks! I guess I'll experiment with a broader refactor and see if anything (else) breaks :)
The test was introduced in 2013, in https://github.com/python/mypy/commit/6ecbd1fd15de2d538410de0e467c0d0ffd343560

Patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -6395,14 +6395,7 @@ def iterable_item_type(self, instance: Instance) -> Type:
             # in case there is no explicit base class.
             return item_type
         # Try also structural typing.
-        ret_type, _ = self.expr_checker.check_method_call_by_name(
-            "__iter__", instance, [], [], instance
-        )
-        ret_type = get_proper_type(ret_type)
-        if isinstance(ret_type, Instance):
-            iterator = map_instance_to_supertype(ret_type, self.lookup_typeinfo("typing.Iterator"))
-            item_type = iterator.args[0]
-        return item_type
+        return self.analyze_iterable_item_type_without_expression(instance, instance)[1]
 
     def function_type(self, func: FuncBase) -> FunctionLike:
         return function_type(func, self.named_type("builtins.function"))


Test patch:
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -380,6 +380,8 @@ class Nums(Iterable[int]):
     def __iter__(self): pass
     def __next__(self): pass
 a, b = Nums()
+reveal_type(a)  # N: Revealed type is "builtins.int"
+reveal_type(b)  # N: Revealed type is "builtins.int"
 if int():
     a = b = 1
 if int():
@@ -388,6 +390,37 @@ if int():
     b = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
 [builtins fixtures/for.pyi]
 
+[case testInferringTypesFromIterableStructuralSubtyping1]
+from typing import Iterator
+class Nums:
+    def __iter__(self) -> Iterator[int]: pass
+a, b = Nums()
+reveal_type(a)  # N: Revealed type is "builtins.int"
+reveal_type(b)  # N: Revealed type is "builtins.int"
+if int():
+    a = b = 1
+if int():
+    a = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+if int():
+    b = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+[builtins fixtures/for.pyi]
+
+[case testInferringTypesFromIterableStructuralSubtyping2]
+from typing import Self
+class Nums:
+    def __iter__(self) -> Self: pass
+    def __next__(self) -> int: pass
+a, b = Nums()
+reveal_type(a)  # N: Revealed type is "builtins.int"
+reveal_type(b)  # N: Revealed type is "builtins.int"
+if int():
+    a = b = 1
+if int():
+    a = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+if int():
+    b = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
+[builtins fixtures/tuple.pyi]
+
 
 -- Type variable inference for generic functions
 -- ---------------------------------------------