crates/ty_python_semantic/resources/mdtest/loops/for.md
for loopclass IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
for x in IntIterable():
pass
# revealed: int
# error: [possibly-unresolved-reference]
reveal_type(x)
class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
x = "foo"
for x in IntIterable():
pass
reveal_type(x) # revealed: Literal["foo"] | int
else (no break)class IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
for x in IntIterable():
pass
else:
x = "foo"
reveal_type(x) # revealed: Literal["foo"]
breakclass IntIterator:
def __next__(self) -> int:
return 42
class IntIterable:
def __iter__(self) -> IntIterator:
return IntIterator()
for x in IntIterable():
if x > 5:
break
else:
x = "foo"
reveal_type(x) # revealed: int | Literal["foo"]
class OldStyleIterable:
def __getitem__(self, key: int) -> int:
return 42
for x in OldStyleIterable():
pass
# revealed: int
# error: [possibly-unresolved-reference]
reveal_type(x)
for x in (1, "a", b"foo"):
pass
# revealed: Literal[1, "a", b"foo"]
# error: [possibly-unresolved-reference]
reveal_type(x)
def _(flag: bool):
class NotIterable:
if flag:
__iter__: int = 1
else:
__iter__: None = None
# error: [not-iterable]
for x in NotIterable():
pass
# revealed: Unknown
# error: [possibly-unresolved-reference]
reveal_type(x)
nonsense = 123
for x in nonsense: # error: [not-iterable]
pass
class NotIterable:
def __getitem__(self, key: int) -> int:
return 42
__iter__: None = None
for x in NotIterable(): # error: [not-iterable]
pass
class TestIter:
def __next__(self) -> int:
return 42
class Test:
def __iter__(self) -> TestIter:
return TestIter()
class Test2:
def __iter__(self) -> TestIter:
return TestIter()
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
class TestIter:
def __next__(self) -> int:
return 42
class TestIter2:
def __next__(self) -> int:
return 42
class Test:
def __iter__(self) -> TestIter | TestIter2:
return TestIter()
for x in Test():
reveal_type(x) # revealed: int
class Result1A: ...
class Result1B: ...
class Result2A: ...
class Result2B: ...
class Result3: ...
class Result4: ...
class TestIter1:
def __next__(self) -> Result1A | Result1B:
return Result1B()
class TestIter2:
def __next__(self) -> Result2A | Result2B:
return Result2B()
class TestIter3:
def __next__(self) -> Result3:
return Result3()
class TestIter4:
def __next__(self) -> Result4:
return Result4()
class Test:
def __iter__(self) -> TestIter1 | TestIter2:
return TestIter1()
class Test2:
def __iter__(self) -> TestIter3 | TestIter4:
return TestIter3()
def _(flag: bool):
for x in Test() if flag else Test2():
reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B | Result3 | Result4
Iterator[] is used as the return type of __iter__This test differs from the above tests in that Iterator (an abstract type) is used as the return
annotation of the __iter__ methods, rather than a concrete type being used as the return
annotation.
from typing import Iterator, Literal
class IntIterator:
def __iter__(self) -> Iterator[int]:
return iter(range(42))
class StrIterator:
def __iter__(self) -> Iterator[str]:
return iter("foo")
def f(x: IntIterator | StrIterator):
for a in x:
reveal_type(a) # revealed: int | str
Most real-world iterable types use Iterator as the return annotation of their __iter__ methods:
def g(
a: tuple[int, ...] | tuple[str, ...],
b: list[str] | list[int],
c: Literal["foo", b"bar"],
):
for x in a:
reveal_type(x) # revealed: int | str
for y in b:
reveal_type(y) # revealed: str | int
If all elements in a union can be iterated over, we "union together" their "tuple specs" and are
able to infer the iterable element precisely when iterating over the union, in the same way that we
infer a precise type for the iterable element when iterating over a Literal string or bytes type:
from typing import Literal
def f(x: Literal["foo", b"bar"], y: Literal["foo"] | range):
for item in x:
reveal_type(item) # revealed: Literal["f", "o", 98, 97, 114]
for item in y:
reveal_type(item) # revealed: Literal["f", "o"] | int
We should still report missing attributes when a loop variable comes from an aliased union element:
[environment]
python-version = "3.12"
class A:
pass
class B:
def do_b_thing(self) -> None:
pass
type U = A | B
class C:
def __init__(self, values: list[U]) -> None:
self.values = values
def f(self) -> None:
for item in self.values:
reveal_type(item) # revealed: A | B
# error: [unresolved-attribute] "Attribute `do_b_thing` is not defined on `A` in union `U`"
item.do_b_thing()
__iter__ methodclass TestIter:
def __next__(self) -> int:
return 42
class Test:
def __iter__(self) -> TestIter:
return TestIter()
def _(flag: bool):
# error: [not-iterable]
for x in Test() if flag else 42:
reveal_type(x) # revealed: int
__iter__ methodclass TestIter:
def __next__(self) -> int:
return 42
class Test:
def __iter__(self) -> TestIter:
return TestIter()
class Test2:
def __iter__(self) -> int:
return 42
def _(flag: bool):
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
# error: [not-iterable]
for x in Test() if flag else Test2():
reveal_type(x) # revealed: int
__iter__When one union element has a callable __iter__ and another has a non-callable __iter__
attribute, the error should be "may not be iterable" (hedged), not "is not iterable" (definitive) —
because at runtime the value might be the iterable variant.
class TestIter:
def __next__(self) -> int:
return 42
class Test:
def __iter__(self) -> TestIter:
return TestIter()
class NotIter:
# `__iter__` is present but not callable
__iter__: int = 32
def _(flag: bool):
iterable = Test() if flag else NotIter()
# error: [not-iterable]
for x in iterable:
reveal_type(x) # revealed: int | Unknown
__next__ methodclass TestIter:
def __next__(self) -> int:
return 42
class Test:
def __iter__(self) -> TestIter | int:
return TestIter()
# error: [not-iterable] "Object of type `Test` may not be iterable"
for x in Test():
reveal_type(x) # revealed: int
When we have an intersection type via isinstance narrowing, we should be able to infer the
iterable element type precisely:
from typing import Sequence
def _(x: Sequence[int], y: object):
reveal_type(x) # revealed: Sequence[int]
for item in x:
reveal_type(item) # revealed: int
if isinstance(y, list):
reveal_type(y) # revealed: Top[list[Unknown]]
for item in y:
reveal_type(item) # revealed: object
if isinstance(x, list):
reveal_type(x) # revealed: Sequence[int] & Top[list[Unknown]]
for item in x:
# int & object simplifies to int
reveal_type(item) # revealed: int
When iterating over an intersection type, we should only fail if all positive elements fail to iterate. If some elements are iterable and some are not, we should iterate over the iterable ones and intersect their element types.
from ty_extensions import Intersection
class NotIterable:
pass
def _(x: Intersection[list[int], NotIterable]):
# `list[int]` is iterable (yielding `int`), but `NotIterable` is not.
# We should still be able to iterate over the intersection.
for item in x:
reveal_type(item) # revealed: int
When iterating over an intersection type where all positive elements are not iterable, we should fail to iterate.
from ty_extensions import Intersection
class NotIterable1:
pass
class NotIterable2:
pass
def _(x: Intersection[NotIterable1, NotIterable2]):
# error: [not-iterable]
for item in x:
reveal_type(item) # revealed: Unknown
When iterating over an intersection of two fixed-length tuples with the same length, we should intersect the element types position-by-position.
from ty_extensions import Intersection
def _(x: Intersection[tuple[int, str], tuple[object, object]]):
# `tuple[int, str]` yields `int | str` when iterated.
# `tuple[object, object]` yields `object` when iterated.
# The intersection should yield `(int & object) | (str & object)` = `int | str`.
for item in x:
reveal_type(item) # revealed: int | str
When iterating over an intersection of a variable-length tuple with a fixed-length tuple, we should preserve the fixed-length structure and intersect each element type with the variable-length tuple's element type.
from ty_extensions import Intersection
def _(x: Intersection[tuple[str, ...], tuple[object, object]]):
# `tuple[str, ...]` yields `str` when iterated.
# `tuple[object, object]` yields `object` when iterated.
# The intersection should yield `(str & object) | (str & object)` = `str`.
for item in x:
reveal_type(item) # revealed: str
When iterating over an intersection of two variable-length tuples, we should intersect the element types position-by-position.
[environment]
python-version = "3.11"
from ty_extensions import Intersection
def _(x: Intersection[tuple[int, *tuple[str, ...], bytes], tuple[object, *tuple[str, ...]]]):
# After resizing, the intersection becomes:
# tuple[int & object, *tuple[str & str, ...], bytes & str]
# = tuple[int, *tuple[str, ...], Never]
# Iterating yields: int | str | Never = int | str
for item in x:
reveal_type(item) # revealed: int | str
When iterating over an intersection of a fixed-length tuple with a class that implements __iter__
returning a homogeneous iterator, we should preserve the fixed-length structure and intersect each
element type with the iterator's element type.
from collections.abc import Iterator
class Foo:
def __iter__(self) -> Iterator[object]:
raise NotImplementedError
def _(x: tuple[int, str, bytes]):
if isinstance(x, Foo):
# The intersection `tuple[int, str, bytes] & Foo` should iterate as
# `tuple[int & object, str & object, bytes & object]` = `tuple[int, str, bytes]`
a, b, c = x
reveal_type(a) # revealed: int
reveal_type(b) # revealed: str
reveal_type(c) # revealed: bytes
reveal_type(tuple(x)) # revealed: tuple[int, str, bytes]
When iterating over an intersection of two types that both yield homogeneous variable-length tuple specs, we should intersect their element types.
from collections.abc import Iterator
class Foo:
def __iter__(self) -> Iterator[object]:
raise NotImplementedError
def _(x: list[int]):
if isinstance(x, Foo):
# `list[int]` yields `int`, `Foo` yields `object`.
# The intersection should yield `int & object` = `int`.
for item in x:
reveal_type(item) # revealed: int
__iter__ methoddef _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class CustomCallable:
if flag:
def __call__(self, *args, **kwargs) -> Iterator:
return Iterator()
else:
__call__: None = None
class Iterable1:
__iter__: CustomCallable = CustomCallable()
class Iterable2:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
__iter__: None = None
# error: [not-iterable] "Object of type `Iterable1` may not be iterable"
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable] "Object of type `Iterable2` may not be iterable"
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
__iter__ method with a bad signatureclass Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self, extra_arg) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int
__iter__ does not return an iteratorclass Bad:
def __iter__(self) -> int:
return 42
# error: [not-iterable]
for x in Bad():
reveal_type(x) # revealed: Unknown
__iter__ returns an object with a possibly missing __next__ methoddef _(flag: bool):
class Iterator:
if flag:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable] "Object of type `Iterable` may not be iterable"
for x in Iterable():
reveal_type(x) # revealed: int
__iter__ returns an iterator with an invalid __next__ methodclass Iterator1:
def __next__(self, extra_arg) -> int:
return 42
class Iterator2:
__next__: None = None
class Iterable1:
def __iter__(self) -> Iterator1:
return Iterator1()
class Iterable2:
def __iter__(self) -> Iterator2:
return Iterator2()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: Unknown
__iter__ and bad __getitem__ methoddef _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
# invalid signature because it only accepts a `str`,
# but the old-style iteration protocol will pass it an `int`
def __getitem__(self, key: str) -> bytes:
return bytes()
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int | bytes
__iter__ and not-callable __getitem__This snippet tests that we infer the element type correctly in the following edge case:
__iter__ is a method with the correct parameter spec that returns a valid iterator; BUT__iter__ is possibly missing; AND__getitem__ is set to a non-callable typeIt's important that we emit a diagnostic here, but it's also important that we still use the return
type of the iterator's __next__ method as the inferred type of x in the for loop:
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
__getitem__: None = None
# error: [not-iterable] "Object of type `Iterable` may not be iterable"
for x in Iterable():
reveal_type(x) # revealed: int
__iter__ and possibly missing __getitem__class Iterator:
def __next__(self) -> int:
return 42
def _(flag1: bool, flag2: bool):
class Iterable:
if flag1:
def __iter__(self) -> Iterator:
return Iterator()
if flag2:
def __getitem__(self, key: int) -> bytes:
return bytes()
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int | bytes
__iter__ method and __getitem__ is not callableclass Bad:
__getitem__: None = None
# error: [not-iterable]
for x in Bad():
reveal_type(x) # revealed: Unknown
__getitem__ methoddef _(flag: bool):
class CustomCallable:
if flag:
def __call__(self, *args, **kwargs) -> int:
return 42
else:
__call__: None = None
class Iterable1:
__getitem__: CustomCallable = CustomCallable()
class Iterable2:
if flag:
def __getitem__(self, key: int) -> int:
return 42
else:
__getitem__: None = None
# error: [not-iterable]
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable]
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
__getitem__ methodclass Iterable:
# invalid because it will implicitly be passed an `int`
# by the interpreter
def __getitem__(self, key: str) -> int:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int
__iter__ but definitely bound __getitem__Here, we should not emit a diagnostic: if __iter__ is unbound, we should fallback to
__getitem__:
class Iterator:
def __next__(self) -> str:
return "foo"
def _(flag: bool):
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
def __getitem__(self, key: int) -> bytes:
return b"foo"
for x in Iterable():
reveal_type(x) # revealed: str | bytes
__iter__ methodsclass Iterator:
def __next__(self) -> int:
return 42
def _(flag: bool):
class Iterable1:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
def __iter__(self, invalid_extra_arg) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int
class Iterable2:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
__iter__: None = None
# error: [not-iterable]
for x in Iterable2():
# TODO: `int` would probably be better here:
reveal_type(x) # revealed: int | Unknown
__next__ methoddef _(flag: bool):
class Iterator1:
if flag:
def __next__(self) -> int:
return 42
else:
def __next__(self, invalid_extra_arg) -> str:
return "foo"
class Iterator2:
if flag:
def __next__(self) -> int:
return 42
else:
__next__: None = None
class Iterable1:
def __iter__(self) -> Iterator1:
return Iterator1()
class Iterable2:
def __iter__(self) -> Iterator2:
return Iterator2()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int | str
# error: [not-iterable]
for y in Iterable2():
# TODO: `int` would probably be better here:
reveal_type(y) # revealed: int | Unknown
__getitem__ methodsdef _(flag: bool):
class Iterable1:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
__getitem__: None = None
class Iterable2:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
def __getitem__(self, item: str) -> int:
return 42
# error: [not-iterable]
for x in Iterable1():
# TODO: `str` might be better
reveal_type(x) # revealed: str | Unknown
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: str | int
__iter__ and possibly invalid __getitem__class Iterator:
def __next__(self) -> bytes:
return b"foo"
def _(flag: bool, flag2: bool):
class Iterable1:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
__getitem__: None = None
if flag2:
def __iter__(self) -> Iterator:
return Iterator()
class Iterable2:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
def __getitem__(self, item: str) -> int:
return 42
if flag2:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable1():
# TODO: `bytes | str` might be better
reveal_type(x) # revealed: bytes | str | Unknown
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: bytes | str | int
for x in ():
reveal_type(x) # revealed: Never
from typing_extensions import Never
def f(never: Never):
for x in never:
reveal_type(x) # revealed: Unknown
from typing import Literal
for char in "abcde":
reveal_type(char) # revealed: Literal["a", "b", "c", "d", "e"]
for char in b"abcde":
reveal_type(char) # revealed: Literal[97, 98, 99, 100, 101]
AnyA class literal can be iterated over if it has Any or Unknown in its MRO, since the
Any/Unknown element in the MRO could materialize to a class with a custom metaclass that defines
__iter__ for all instances of the metaclass:
from unresolved_module import SomethingUnknown # error: [unresolved-import]
from typing import Any, Iterable
from ty_extensions import static_assert, is_assignable_to, TypeOf, Unknown, reveal_mro
class Foo(SomethingUnknown): ...
reveal_mro(Foo) # revealed: (<class 'Foo'>, Unknown, <class 'object'>)
# TODO: these should pass
static_assert(is_assignable_to(TypeOf[Foo], Iterable[Unknown])) # error: [static-assert-error]
static_assert(is_assignable_to(type[Foo], Iterable[Unknown])) # error: [static-assert-error]
# TODO: should not error
# error: [not-iterable]
for x in Foo:
reveal_type(x) # revealed: Unknown
class Bar(Any): ...
reveal_mro(Bar) # revealed: (<class 'Bar'>, Any, <class 'object'>)
# TODO: these should pass
static_assert(is_assignable_to(TypeOf[Bar], Iterable[Any])) # error: [static-assert-error]
static_assert(is_assignable_to(type[Bar], Iterable[Any])) # error: [static-assert-error]
# TODO: should not error
# error: [not-iterable]
for x in Bar:
# TODO: should reveal `Any`
reveal_type(x) # revealed: Unknown
When a TypeVar has a union bound and the TypeVar is intersected with an iterable type (e.g., via
isinstance), we need to properly distribute the intersection over the union and simplify. This
ensures that only the parts of the union compatible with the intersection are considered for
iteration.
[environment]
python-version = "3.12"
When the union contains non-iterable types (like int), those parts are disjoint from the tuple and
simplify to Never, leaving only the iterable parts.
def f[T: tuple[int, ...] | int](x: T):
if isinstance(x, tuple):
reveal_type(x) # revealed: T@f & tuple[object, ...]
for item in x:
# The intersection `(tuple[int, ...] | int) & tuple[object, ...]` distributes to:
# `(tuple[int, ...] & tuple[object, ...]) | (int & tuple[object, ...])`
# which simplifies to `tuple[int, ...] | Never` = `tuple[int, ...]`
# so iterating gives `int`.
reveal_type(item) # revealed: int
When the union contains types that are all iterable but some are disjoint from the intersection
constraint, those parts should also simplify to Never.
def g[T: tuple[int, ...] | list[str]](x: T):
if isinstance(x, tuple):
reveal_type(x) # revealed: T@g & tuple[object, ...]
for item in x:
# The intersection `(tuple[int, ...] | list[str]) & tuple[object, ...]` distributes to:
# `(tuple[int, ...] & tuple[object, ...]) | (list[str] & tuple[object, ...])`
# Since `list[str]` is disjoint from `tuple[object, ...]`, this simplifies to:
# `tuple[int, ...] | Never` = `tuple[int, ...]`
# so iterating gives `int`, NOT `int | str`.
reveal_type(item) # revealed: int
When we have a list with a negated type parameter (e.g., list[~str]), we should still be able to
iterate over it correctly. The negated type parameter represents all types except str, and
list[~str] is still a valid list that can be iterated.
from ty_extensions import Not
def _(value: list[Not[str]]):
for x in value:
reveal_type(x) # revealed: ~str
for _ in (x := range(0)):
pass
reveal_type(x) # revealed: range
i = 0
reveal_type(i) # revealed: Literal[0]
for _ in range(1_000_000):
i += 1
reveal_type(i) # revealed: int
reveal_type(i) # revealed: int
i = 0
for _ in range(1_000_000):
if i > 0:
loop_only += 1 # error: [possibly-unresolved-reference]
if i == 0:
loop_only = 0
i += 1
# error: [possibly-unresolved-reference]
reveal_type(loop_only) # revealed: int
break and continuedef random() -> bool:
return False
x = "A"
for _ in range(1_000_000):
reveal_type(x) # revealed: Literal["A", "D"]
for _ in range(1_000_000):
# The "C" binding isn't visible here. It breaks this inner loop, and it always gets
# overwritten before the end of the outer loop.
reveal_type(x) # revealed: Literal["A", "D", "B"]
if random():
x = "B"
continue
else:
x = "C"
break
reveal_type(x) # revealed: Never
# We don't know whether a `for` loop will execute its body at all, so "A" is still visible here.
# Similarly, we don't know when the loop will terminate, so "B" is also visible here despite the
# `continue` above.
reveal_type(x) # revealed: Literal["A", "D", "B", "C"]
if random():
x = "D"
continue
else:
x = "E"
break
reveal_type(x) # revealed: Never
reveal_type(x) # revealed: Literal["A", "D", "E"]
for _ in range(1_000_000):
# error: [possibly-unresolved-reference]
reveal_type(y) # revealed: Literal[1]
x = (y := 1)
The iterable is only evaluated once, before the loop body runs.
x = "hello"
for _ in (y := x):
# This assignment is not visible when the iterable `x` is used above.
x = None
reveal_type(y) # revealed: Literal["hello"]
my_dict = {}
my_dict["x"] = 0
reveal_type(my_dict["x"]) # revealed: Literal[0]
for _ in range(1_000_000):
my_dict["x"] += 1
reveal_type(my_dict["x"]) # revealed: int
del prevents bindings from reaching the loopbackThis x cannot reach the use at the top of the loop:
for _ in range(1_000_000):
x # error: [unresolved-reference]
x = 42
del x
On the other hand, if x is defined before the loop, the del makes it a
[possibly-unresolved-reference]:
x = 0
for _ in range(1_000_000):
x # error: [possibly-unresolved-reference]
x = 42
del x
del in a loop makes a variable possibly-unbound after the loopx = 0
for _ in range(1_000_000):
# error: [possibly-unresolved-reference]
del x
# error: [possibly-unresolved-reference]
x
for _ in range(1_000_000):
x = 42
# error: [possibly-unresolved-reference]
x
x = 1
y = 2
for _ in range(1_000_000):
x, y = y, x
reveal_type(x) # revealed: Literal[2, 1]
reveal_type(y) # revealed: Literal[1, 2]
x = 0
for _ in range(1_000_000):
x, y = x + 1, None
reveal_type(x) # revealed: int
We need to avoid oscillating cycles in cases like the following, where the type of one of these loop
variables also influences the static reachability of its bindings. This case was minimized from a
real crash that came up during development checking these lines of sympy:
https://github.com/sympy/sympy/blob/c2bfd65accf956576b58f0ae57bf5821a0c4ff49/sympy/core/numbers.py#L158-L166
x = 1
y = 2
for _ in range(1_000_000):
if x:
x, y = y, x
reveal_type(x) # revealed: Literal[2, 1]
reveal_type(y) # revealed: Literal[1, 2]
VAL = 1
x = 1
for _ in range(1_000_000):
reveal_type(x) # revealed: Literal[1]
if VAL - 1:
x = 2
Divergent in narrowing conditions doesn't run afoul of "monotonic widening" in cycle recoveryThis test looks for a complicated inference failure case that came up during implementation. See the
while variant of this case in while_loop.md for a detailed description.
class Node:
def __init__(self, next: "Node | None" = None):
self.next: "Node | None" = next
node = Node(Node(Node()))
for _ in range(1_000_000):
if node.next is None:
break
node = node.next
reveal_type(node) # revealed: Node
reveal_type(node.next) # revealed: Node | None
global and nonlocal keywords in a loopWe need to make sure that the loop header definition doesn't count as a "use" prior to the
global/nonlocal declaration, or else we'll emit a false-positive semantic syntax error.
x = 0
def _():
y = 0
def _():
for _ in range(1_000_000):
global x
nonlocal y
x = 42
y = 99
On the other hand, we don't want to shadow true positives:
x = 0
def _():
y = 0
def _():
x = 1
y = 1
for _ in range(1_000_000):
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
nonlocal y # error: [invalid-syntax] "name `y` is used prior to nonlocal declaration"
class C:
x = None
c = C()
c.x = 0
for _ in range(1):
reveal_type(c.x) # revealed: Literal[0]
c = C()
break
d = [0]
d[0] = 1
for _ in range(1):
reveal_type(d[0]) # revealed: Literal[1]
d = []
break