Skip to content

Commit 270a01d

Browse files
authored
Fix loop convergence with redefinitions (python#20865)
Currently, we reset the variable type each time we visit the initial definition (textually first assignment). This can cause problems with `--allow-redefinition-new`: * Not all branches will be explored (see added test). This is a correctness problem. * Many loops will become runaway (will never converge to a fixed point) while they actually can converge. Currently this is stopped by manual cut-off. This is a performance problem. The only non-trivial part here is handling of this pattern: ```python for key in ("key_a", "key_b"): some_typed_dict[key] ``` It is special-cased in regular mode, and this special-casing used to work accidentally with redefinition allowed. Now I simply make the same special-casing apply (since for loops can never be runaway on index).
1 parent 4215dfd commit 270a01d

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

mypy/checker.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4401,8 +4401,23 @@ def check_lvalue(
44014401
index_lvalue = None
44024402
inferred = None
44034403

4404-
if self.is_definition(lvalue) and (
4405-
not isinstance(lvalue, NameExpr) or isinstance(lvalue.node, Var)
4404+
# When revisiting the initial assignment (for example in a loop),
4405+
# treat is as regular if redefinitions are allowed.
4406+
skip_definition = (
4407+
self.options.allow_redefinition_new
4408+
and isinstance(lvalue, NameExpr)
4409+
and isinstance(lvalue.node, Var)
4410+
and lvalue.node.is_inferred
4411+
and lvalue.node.type is not None
4412+
# Indexes in for loops require special handling, we need to reset them to
4413+
# a literal value on each loop, but binder doesn't work well with literals.
4414+
and not lvalue.node.is_index_var
4415+
)
4416+
4417+
if (
4418+
self.is_definition(lvalue)
4419+
and (not isinstance(lvalue, NameExpr) or isinstance(lvalue.node, Var))
4420+
and not skip_definition
44064421
):
44074422
if isinstance(lvalue, NameExpr):
44084423
assert isinstance(lvalue.node, Var)
@@ -4784,7 +4799,7 @@ def check_simple_assignment(
47844799
# This additional check is to give an error instead of inferring
47854800
# a useless type like None | list[Never] in case of "double-partial"
47864801
# types that are not supported yet, see issue #20257.
4787-
or not is_proper_subtype(rvalue_type, inferred.type)
4802+
or not is_subtype(rvalue_type, inferred.type)
47884803
)
47894804
):
47904805
self.msg.need_annotation_for_var(inferred, inferred, self.options)
@@ -4807,7 +4822,9 @@ def check_simple_assignment(
48074822
):
48084823
lvalue_type = make_simplified_union([inferred.type, new_inferred])
48094824
# Widen the type to the union of original and new type.
4810-
self.widened_vars.append(inferred.name)
4825+
if not inferred.is_index_var:
4826+
# Skip index variables as they are reset on each loop.
4827+
self.widened_vars.append(inferred.name)
48114828
self.set_inferred_type(inferred, lvalue, lvalue_type)
48124829
self.binder.put(lvalue, rvalue_type)
48134830
# TODO: A bit hacky, maybe add a binder method that does put and

test-data/unit/check-redefine2.test

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,33 @@ def func() -> None:
10981098
reveal_type(l) # N: Revealed type is "None | builtins.list[Any]"
10991099
[builtins fixtures/list.pyi]
11001100

1101+
[case testNewRedefineLoopProgressiveWidening]
1102+
# flags: --allow-redefinition-new --local-partial-types
1103+
def foo() -> None:
1104+
while int():
1105+
if int():
1106+
x = 1
1107+
elif isinstance(x, list):
1108+
x = ""
1109+
elif isinstance(x, str):
1110+
x = None
1111+
else:
1112+
x = [1]
1113+
reveal_type(x) # N: Revealed type is "builtins.int | builtins.list[builtins.int] | builtins.str | None"
1114+
[builtins fixtures/isinstancelist.pyi]
1115+
1116+
[case testNewRedefineAnyOrEmptyUnpackInLoop]
1117+
# flags: --allow-redefinition-new --local-partial-types
1118+
from typing import Any
1119+
1120+
def foo() -> None:
1121+
while int():
1122+
data: Any
1123+
x, y = data or ([], [])
1124+
# Requiring an annotation may be better, but we want to preserve old behaviour.
1125+
reveal_type(x) # N: Revealed type is "Any | builtins.list[Never]"
1126+
[builtins fixtures/isinstancelist.pyi]
1127+
11011128
[case testNewRedefineNarrowingSpecialCase]
11021129
# flags: --allow-redefinition-new --local-partial-types --warn-unreachable
11031130
from typing import Any, Union

0 commit comments

Comments
 (0)