Skip to content

Commit 34e8c7c

Browse files
authored
Add flag to allow more flexible variable redefinition (#18727)
Infer union types for simple variables from multiple assignments, if the variable isn't annotated. The feature is enabled via `--allow-redefinition-new`. `--local-partial-types` must also be enabled. This is still experimental and has known issues, so it's not documented anywhere. It works well enough that it can be used for non-trivial experimentation, however. Closes #6233. Closes #6232. Closes #18568. Fixes #18619. In this example, the type of `x` is inferred as `int | str` when using the new behavior: ```py def f(i: int, s : str) -> int | str: if i > 5: x = i else: x = s # No longer an error reveal_type(x) # int | str return s ``` Here is a summary of how it works: * Assignment widens the inferred type of a variable and always narrows (when there is no annotation). * Simple variable lvalues are put into the binder on initial assignment when using the new feature. We need to be able to track whether a variable is defined or not to infer correct types (see #18619). * Assignment of `None` values are no longer special, and we don't use partial None if the feature is enabled for simple variables. * Lvalues other than simple variables (e.g. `self.x`) continue to work as in the past. Attribute types can't be widened, since they are externally visible and widening could cause confusion, but this is something we might relax in the future. Globals can be widened, however. This seems necessary for consistency. * If a loop body widens a variable type, we have to analyze the body again. However, we only do one extra pass, since the inferred type could be expanded without bound (consider `x = 0` outside loop and `x = [x]` within the loop body). * We first infer the type of an rvalue without using the lvalue type as context, as otherwise the type context would often prevent redefinition. If the rvalue type isn't valid for inference (e.g. list item type can't be inferred), we fall back to the lvalue type context. There are some other known bugs and limitations: * Annotated variables can't be freely redefined (but they can still be narrowed, of course). I may want to relax this in the future, but I'm not sure yet. * If there is a function definition between assignments to a variable, the inferred types may be incorrect. * There are few tests for `nonlocal` and some other features. We don't have good test coverage for deferrals, mypy daemon, and disabling strict optional. * Imported names can't be redefined in a consistent way. This needs further analysis. In self check the feature generates 6 additional errors, which all seem correct -- we infer more precise types, which will generate additional errors due to invariant containers and fixing false negatives. When type checking the largest internal codebase at Dropbox, this generated about 700 new errors, the vast majority of which seemed legitimate. Mostly they were due to inferring more precise types for variables that used to have `Any` types. I used a recent but not the latest version of the feature to type check the internal codebase.
1 parent 04a0fe8 commit 34e8c7c

12 files changed

+1510
-54
lines changed

mypy/binder.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from typing_extensions import TypeAlias as _TypeAlias
88

99
from mypy.erasetype import remove_instance_last_known_values
10-
from mypy.literals import Key, literal, literal_hash, subkeys
10+
from mypy.literals import Key, extract_var_from_literal_hash, literal, literal_hash, subkeys
1111
from mypy.nodes import Expression, IndexExpr, MemberExpr, NameExpr, RefExpr, TypeInfo, Var
12+
from mypy.options import Options
1213
from mypy.subtypes import is_same_type, is_subtype
1314
from mypy.typeops import make_simplified_union
1415
from mypy.types import (
@@ -39,6 +40,7 @@ class CurrentType(NamedTuple):
3940

4041
class Frame:
4142
"""A Frame represents a specific point in the execution of a program.
43+
4244
It carries information about the current types of expressions at
4345
that point, arising either from assignments to those expressions
4446
or the result of isinstance checks and other type narrowing
@@ -97,7 +99,7 @@ class A:
9799
# This maps an expression to a list of bound types for every item in the union type.
98100
type_assignments: Assigns | None = None
99101

100-
def __init__(self) -> None:
102+
def __init__(self, options: Options) -> None:
101103
# Each frame gets an increasing, distinct id.
102104
self.next_id = 1
103105

@@ -131,6 +133,11 @@ def __init__(self) -> None:
131133
self.break_frames: list[int] = []
132134
self.continue_frames: list[int] = []
133135

136+
# If True, initial assignment to a simple variable (e.g. "x", but not "x.y")
137+
# is added to the binder. This allows more precise narrowing and more
138+
# flexible inference of variable types (--allow-redefinition-new).
139+
self.bind_all = options.allow_redefinition_new
140+
134141
def _get_id(self) -> int:
135142
self.next_id += 1
136143
return self.next_id
@@ -226,12 +233,20 @@ def update_from_options(self, frames: list[Frame]) -> bool:
226233
for key in keys:
227234
current_value = self._get(key)
228235
resulting_values = [f.types.get(key, current_value) for f in frames]
229-
if any(x is None for x in resulting_values):
236+
# Keys can be narrowed using two different semantics. The new semantics
237+
# is enabled for plain variables when bind_all is true, and it allows
238+
# variable types to be widened using subsequent assignments. This is
239+
# tricky to support for instance attributes (primarily due to deferrals),
240+
# so we don't use it for them.
241+
old_semantics = not self.bind_all or extract_var_from_literal_hash(key) is None
242+
if old_semantics and any(x is None for x in resulting_values):
230243
# We didn't know anything about key before
231244
# (current_value must be None), and we still don't
232245
# know anything about key in at least one possible frame.
233246
continue
234247

248+
resulting_values = [x for x in resulting_values if x is not None]
249+
235250
if all_reachable and all(
236251
x is not None and not x.from_assignment for x in resulting_values
237252
):
@@ -278,7 +293,11 @@ def update_from_options(self, frames: list[Frame]) -> bool:
278293
# still equivalent to such type).
279294
if isinstance(type, UnionType):
280295
type = collapse_variadic_union(type)
281-
if isinstance(type, ProperType) and isinstance(type, UnionType):
296+
if (
297+
old_semantics
298+
and isinstance(type, ProperType)
299+
and isinstance(type, UnionType)
300+
):
282301
# Simplify away any extra Any's that were added to the declared
283302
# type when popping a frame.
284303
simplified = UnionType.make_union(

mypy/build.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2240,8 +2240,10 @@ def semantic_analysis_pass1(self) -> None:
22402240
# TODO: Do this while constructing the AST?
22412241
self.tree.names = SymbolTable()
22422242
if not self.tree.is_stub:
2243-
# Always perform some low-key variable renaming
2244-
self.tree.accept(LimitedVariableRenameVisitor())
2243+
if not self.options.allow_redefinition_new:
2244+
# Perform some low-key variable renaming when assignments can't
2245+
# widen inferred types
2246+
self.tree.accept(LimitedVariableRenameVisitor())
22452247
if options.allow_redefinition:
22462248
# Perform more renaming across the AST to allow variable redefinitions
22472249
self.tree.accept(VariableRenameVisitor())

0 commit comments

Comments
 (0)