Skip to content

Commit

Permalink
Fix Any inference when unpacking iterators that don't directly inhe…
Browse files Browse the repository at this point in the history
…rit from `typing.Iterator` (#14821)

Fixes #14819.

Mypy currently silently infers an `Any` type when unpacking an iterator
that doesn't explicitly inherit from `typing.Iterator` (i.e., an
iterator that's a _structural_ subtype of `typing.Iterator`, but not a
_nominal_ subtype):

```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"
```

However, we have enough information here to infer that the type of `a`
should really be `int`. This PR fixes that bug.

There's discussion on the issue thread about an alternative solution
that would involve changing some mypy behaviour that's been established
for around 10 years. For now, I haven't gone for that solution.
  • Loading branch information
AlexWaygood committed Mar 3, 2023
1 parent 099500e commit 456dcbd
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 8 deletions.
9 changes: 1 addition & 8 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
33 changes: 33 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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
-- ---------------------------------------------
Expand Down

0 comments on commit 456dcbd

Please sign in to comment.