Skip to content

Commit 456dcbd

Browse files
authored
Fix Any inference when unpacking iterators that don't directly inherit 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.
1 parent 099500e commit 456dcbd

File tree

2 files changed

+34
-8
lines changed

2 files changed

+34
-8
lines changed

mypy/checker.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6395,14 +6395,7 @@ def iterable_item_type(self, instance: Instance) -> Type:
63956395
# in case there is no explicit base class.
63966396
return item_type
63976397
# Try also structural typing.
6398-
ret_type, _ = self.expr_checker.check_method_call_by_name(
6399-
"__iter__", instance, [], [], instance
6400-
)
6401-
ret_type = get_proper_type(ret_type)
6402-
if isinstance(ret_type, Instance):
6403-
iterator = map_instance_to_supertype(ret_type, self.lookup_typeinfo("typing.Iterator"))
6404-
item_type = iterator.args[0]
6405-
return item_type
6398+
return self.analyze_iterable_item_type_without_expression(instance, instance)[1]
64066399

64076400
def function_type(self, func: FuncBase) -> FunctionLike:
64086401
return function_type(func, self.named_type("builtins.function"))

test-data/unit/check-inference.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ class Nums(Iterable[int]):
380380
def __iter__(self): pass
381381
def __next__(self): pass
382382
a, b = Nums()
383+
reveal_type(a) # N: Revealed type is "builtins.int"
384+
reveal_type(b) # N: Revealed type is "builtins.int"
383385
if int():
384386
a = b = 1
385387
if int():
@@ -388,6 +390,37 @@ if int():
388390
b = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
389391
[builtins fixtures/for.pyi]
390392

393+
[case testInferringTypesFromIterableStructuralSubtyping1]
394+
from typing import Iterator
395+
class Nums:
396+
def __iter__(self) -> Iterator[int]: pass
397+
a, b = Nums()
398+
reveal_type(a) # N: Revealed type is "builtins.int"
399+
reveal_type(b) # N: Revealed type is "builtins.int"
400+
if int():
401+
a = b = 1
402+
if int():
403+
a = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
404+
if int():
405+
b = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
406+
[builtins fixtures/for.pyi]
407+
408+
[case testInferringTypesFromIterableStructuralSubtyping2]
409+
from typing import Self
410+
class Nums:
411+
def __iter__(self) -> Self: pass
412+
def __next__(self) -> int: pass
413+
a, b = Nums()
414+
reveal_type(a) # N: Revealed type is "builtins.int"
415+
reveal_type(b) # N: Revealed type is "builtins.int"
416+
if int():
417+
a = b = 1
418+
if int():
419+
a = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
420+
if int():
421+
b = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
422+
[builtins fixtures/tuple.pyi]
423+
391424

392425
-- Type variable inference for generic functions
393426
-- ---------------------------------------------

0 commit comments

Comments
 (0)