From 8f06769509cb33cad77fb634f3eed6f6e45c7573 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 16:58:28 +0100 Subject: [PATCH 01/11] WIP narrowing global in function with --allow-redefinition --- mypy/checker.py | 32 +++++++++++++++++++++++++++-- test-data/unit/check-redefine2.test | 31 +++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 59571954e0f72..58d944826e9e3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -424,6 +424,9 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi, SplittingVisitor): # Short names of Var nodes whose previous inferred type has been widened via assignment. # NOTE: The names might not be unique, they are only for debugging purposes. widened_vars: list[str] + # Global variables widened inside a function body, to be propagated to + # the module-level binder after the function is type checked. + _globals_widened_in_func: list[tuple[NameExpr, Type]] globals: SymbolTable modules: dict[str, MypyFile] # Nodes that couldn't be checked because some types weren't available. We'll run @@ -496,6 +499,7 @@ def __init__( self.var_decl_frames = {} self.deferred_nodes = [] self.widened_vars = [] + self._globals_widened_in_func = [] self._type_maps = [{}] self.module_refs = set() self.pass_num = 0 @@ -1616,6 +1620,15 @@ def check_func_def( self.return_types.pop() + # Propagate any global variable widenings to the outer binder. + if self._globals_widened_in_func: + for lvalue, widened_type in self._globals_widened_in_func: + old_binder.put(lvalue, widened_type) + lit = literal_hash(lvalue) + if lit is not None: + old_binder.declarations[lit] = widened_type + self._globals_widened_in_func = [] + self.binder = old_binder def check_funcdef_item( @@ -4904,6 +4917,13 @@ def check_simple_assignment( # Skip index variables as they are reset on each loop. self.widened_vars.append(inferred.name) self.set_inferred_type(inferred, lvalue, lvalue_type) + if ( + lvalue.kind == GDEF + and self.scope.top_level_function() is not None + ): + # Widening a global inside a function -- record for + # propagation to the module-level binder afterwards. + self._globals_widened_in_func.append((lvalue, lvalue_type)) self.binder.put(lvalue, rvalue_type) # TODO: A bit hacky, maybe add a binder method that does put and # updates declaration? @@ -4932,7 +4952,7 @@ def refers_to_different_scope(self, name: NameExpr) -> bool: if name.kind == LDEF: # TODO: Consider reference to outer function as a different scope? return False - elif self.scope.top_level_function() is not None: + elif self.scope.top_level_function() is not None and name.kind != GDEF: # A non-local reference from within a function must refer to a different scope return True elif name.kind == GDEF and name.fullname.rpartition(".")[0] != self.tree.fullname: @@ -8337,7 +8357,15 @@ def visit_nonlocal_decl(self, o: NonlocalDecl, /) -> None: return None def visit_global_decl(self, o: GlobalDecl, /) -> None: - return None + if self.options.allow_redefinition: + for name in o.names: + sym = self.globals.get(name) + if sym and isinstance(sym.node, Var) and sym.node.type is not None: + n = NameExpr(name) + n.node = sym.node + n.kind = GDEF + n.fullname = sym.node.fullname + self.binder.assign_type(n, sym.node.type, sym.node.type) class TypeCheckerAsSemanticAnalyzer(SemanticAnalyzerCoreInterface): diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index d8a7ccbfc4a4d..ce6a9453af892 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -168,17 +168,42 @@ def f2() -> None: reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" -[case testNewRedefineGlobalVariableNoneInit] +[case testNewRedefineGlobalVariableNoneInit1] # flags: --allow-redefinition-new --local-partial-types x = None def f() -> None: global x reveal_type(x) # N: Revealed type is "None" - x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None") + x = 1 + reveal_type(x) # N: Revealed type is "builtins.int" + +reveal_type(x) # N: Revealed type is "None | builtins.int" + +[case testNewRedefineGlobalVariableNoneInit2] +# flags: --allow-redefinition +x = None + +def f() -> None: + global x + if int(): + x = 1 + reveal_type(x) # N: Revealed type is "None | builtins.int" + +reveal_type(x) # N: Revealed type is "None | builtins.int" + +[case testNewRedefineGlobalVariableNoneInit3] +# flags: --allow-redefinition +x = None + +def f() -> None: + global x + x = 1 + a = [x] + x = None reveal_type(x) # N: Revealed type is "None" -reveal_type(x) # N: Revealed type is "None" +reveal_type(x) # N: Revealed type is "None | builtins.int" [case testNewRedefineParameterTypes] # flags: --allow-redefinition-new --local-partial-types From 83e7b759e86c311248aeffcb572c8157e3b766ca Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:06:46 +0100 Subject: [PATCH 02/11] Only None can be widened --- mypy/checker.py | 20 ++++++++++++++++---- test-data/unit/check-redefine2.test | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 58d944826e9e3..61dcd64c93a1c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4910,6 +4910,7 @@ def check_simple_assignment( not self.refers_to_different_scope(lvalue) and not isinstance(inferred.type, PartialType) and not is_proper_subtype(new_inferred, inferred.type) + and self.can_widen_in_scope(lvalue, inferred.type) ): lvalue_type = make_simplified_union([inferred.type, new_inferred]) # Widen the type to the union of original and new type. @@ -4917,10 +4918,7 @@ def check_simple_assignment( # Skip index variables as they are reset on each loop. self.widened_vars.append(inferred.name) self.set_inferred_type(inferred, lvalue, lvalue_type) - if ( - lvalue.kind == GDEF - and self.scope.top_level_function() is not None - ): + if lvalue.kind == GDEF and self.scope.top_level_function() is not None: # Widening a global inside a function -- record for # propagation to the module-level binder afterwards. self._globals_widened_in_func.append((lvalue, lvalue_type)) @@ -4960,6 +4958,20 @@ def refers_to_different_scope(self, name: NameExpr) -> bool: return True return False + def can_widen_in_scope(self, name: NameExpr, orig_type: Type) -> bool: + """Can a variable type be widened via assignment in the current scope? + + Globals can only be widened from within a function if the original type + is None (backward compat with partial type handling of ``x = None``). + """ + if ( + name.kind == GDEF + and self.scope.top_level_function() is not None + and not isinstance(get_proper_type(orig_type), NoneType) + ): + return False + return True + def check_member_assignment( self, lvalue: MemberExpr, diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index ce6a9453af892..fe6cb6565fbda 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -180,6 +180,15 @@ def f() -> None: reveal_type(x) # N: Revealed type is "None | builtins.int" +def g() -> None: + global x + reveal_type(x) # N: Revealed type is "None | builtins.int" + # Can only widen in one function to avoid complex interactions + x = "a" # E: Incompatible types in assignment (expression has type "str", variable has type "int | None") + reveal_type(x) # N: Revealed type is "None | builtins.int" + +reveal_type(x) # N: Revealed type is "None | builtins.int" + [case testNewRedefineGlobalVariableNoneInit2] # flags: --allow-redefinition x = None @@ -205,6 +214,16 @@ def f() -> None: reveal_type(x) # N: Revealed type is "None | builtins.int" +[case testNewRedefineGlobalVariableWithUnsupportedType] +# flags: --allow-redefinition +x = 1 + +def f() -> None: + global x + x = "a" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +reveal_type(x) # N: Revealed type is "builtins.int" + [case testNewRedefineParameterTypes] # flags: --allow-redefinition-new --local-partial-types from typing import Optional From 25170f11331166b4d463d76681ae6337e921511e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:18:30 +0100 Subject: [PATCH 03/11] Update test case --- test-data/unit/check-inference.test | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index c08908eb8a3a3..4653cd349fc6e 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -4353,17 +4353,17 @@ def g() -> None: [case testGlobalVariableNoneInitMultipleFuncsRedefine] # flags: --allow-redefinition-new --local-partial-types -# Widening this is intentionally prohibited (for now). +# Widening None is supported, as a special case x = None def f() -> None: global x reveal_type(x) # N: Revealed type is "None" - x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None") - reveal_type(x) # N: Revealed type is "None" + x = 1 + reveal_type(x) # N: Revealed type is "builtins.int" def g() -> None: global x - reveal_type(x) # N: Revealed type is "None" - x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "None") - reveal_type(x) # N: Revealed type is "None" + reveal_type(x) # N: Revealed type is "None | builtins.int" + x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int | None") + reveal_type(x) # N: Revealed type is "None | builtins.int" From d8921de916022218af5724cbf751bb4fc087ef2a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:21:40 +0100 Subject: [PATCH 04/11] Fix tests --- test-data/unit/check-redefine2.test | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index fe6cb6565fbda..23b41f387c0b1 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -190,7 +190,7 @@ def g() -> None: reveal_type(x) # N: Revealed type is "None | builtins.int" [case testNewRedefineGlobalVariableNoneInit2] -# flags: --allow-redefinition +# flags: --allow-redefinition-new --local-partial-types x = None def f() -> None: @@ -202,7 +202,7 @@ def f() -> None: reveal_type(x) # N: Revealed type is "None | builtins.int" [case testNewRedefineGlobalVariableNoneInit3] -# flags: --allow-redefinition +# flags: --allow-redefinition-new --local-partial-types x = None def f() -> None: @@ -215,7 +215,7 @@ def f() -> None: reveal_type(x) # N: Revealed type is "None | builtins.int" [case testNewRedefineGlobalVariableWithUnsupportedType] -# flags: --allow-redefinition +# flags: --allow-redefinition-new --local-partial-types x = 1 def f() -> None: @@ -685,6 +685,7 @@ def f5() -> None: continue x = "" reveal_type(x) # N: Revealed type is "builtins.str" + [case testNewRedefineWhileLoopSimple] # flags: --allow-redefinition-new --local-partial-types def f() -> None: From 6708f54de47e281eee54992d923488e3313ab6d0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:22:29 +0100 Subject: [PATCH 05/11] Fix after rebase --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 61dcd64c93a1c..359926f996358 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8369,7 +8369,7 @@ def visit_nonlocal_decl(self, o: NonlocalDecl, /) -> None: return None def visit_global_decl(self, o: GlobalDecl, /) -> None: - if self.options.allow_redefinition: + if self.options.allow_redefinition_new: for name in o.names: sym = self.globals.get(name) if sym and isinstance(sym.node, Var) and sym.node.type is not None: From ab18788df898438d2dd9851423faf89efc36ee88 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:25:01 +0100 Subject: [PATCH 06/11] Update tests --- test-data/unit/check-inference.test | 2 ++ test-data/unit/check-redefine2.test | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 4653cd349fc6e..3550d6967ead2 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -4350,6 +4350,8 @@ def g() -> None: x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int | None") reveal_type(x) # N: Revealed type is "builtins.int | None" +reveal_type(x) # N: Revealed type is "builtins.int | None" + [case testGlobalVariableNoneInitMultipleFuncsRedefine] # flags: --allow-redefinition-new --local-partial-types diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 23b41f387c0b1..3e4924244e1e9 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -180,19 +180,13 @@ def f() -> None: reveal_type(x) # N: Revealed type is "None | builtins.int" -def g() -> None: - global x - reveal_type(x) # N: Revealed type is "None | builtins.int" - # Can only widen in one function to avoid complex interactions - x = "a" # E: Incompatible types in assignment (expression has type "str", variable has type "int | None") - reveal_type(x) # N: Revealed type is "None | builtins.int" - -reveal_type(x) # N: Revealed type is "None | builtins.int" - [case testNewRedefineGlobalVariableNoneInit2] # flags: --allow-redefinition-new --local-partial-types x = None +def deco(f): return f + +@deco def f() -> None: global x if int(): @@ -203,11 +197,18 @@ reveal_type(x) # N: Revealed type is "None | builtins.int" [case testNewRedefineGlobalVariableNoneInit3] # flags: --allow-redefinition-new --local-partial-types +from typing import overload + x = None -def f() -> None: +@overload +def f() -> None: ... +@overload +def f(n: int) -> None: ... + +def f(n: int = 0) -> None: global x - x = 1 + x = n a = [x] x = None reveal_type(x) # N: Revealed type is "None" From 8d0376aff6058a55c889e4ebbe6f1afca7bbe68a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:30:36 +0100 Subject: [PATCH 07/11] Fix methods --- mypy/checker.py | 8 +++++--- test-data/unit/check-redefine2.test | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 359926f996358..e063aeb1ab91c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -491,6 +491,7 @@ def __init__( self.tscope = Scope() self.scope = CheckerScope(tree) self.binder = ConditionalTypeBinder(options) + self.globals_binder = self.binder self.globals = tree.names self.return_types = [] self.dynamic_funcs = [] @@ -1620,13 +1621,14 @@ def check_func_def( self.return_types.pop() - # Propagate any global variable widenings to the outer binder. + # Propagate any global variable widenings directly to the + # module-level binder (skipping any intermediate class binders). if self._globals_widened_in_func: for lvalue, widened_type in self._globals_widened_in_func: - old_binder.put(lvalue, widened_type) + self.globals_binder.put(lvalue, widened_type) lit = literal_hash(lvalue) if lit is not None: - old_binder.declarations[lit] = widened_type + self.globals_binder.declarations[lit] = widened_type self._globals_widened_in_func = [] self.binder = old_binder diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 3e4924244e1e9..b7bf4ee681671 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -201,17 +201,18 @@ from typing import overload x = None -@overload -def f() -> None: ... -@overload -def f(n: int) -> None: ... - -def f(n: int = 0) -> None: - global x - x = n - a = [x] - x = None - reveal_type(x) # N: Revealed type is "None" +class C: + @overload + def f(self) -> None: ... + @overload + def f(self, n: int) -> None: ... + + def f(self, n: int = 0) -> None: + global x + x = n + a = [x] + x = None + reveal_type(x) # N: Revealed type is "None" reveal_type(x) # N: Revealed type is "None | builtins.int" From d1bdbfe836d4f9bf48a0a5f48c7a380588740006 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:43:55 +0100 Subject: [PATCH 08/11] Add fine-grained tests --- test-data/unit/fine-grained.test | 56 +++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 671a20b66779c..88ada8683681f 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -6743,7 +6743,7 @@ class D: class D: y: int [out] -b2.py:7: error: Argument 1 to "f" has incompatible type "D"; expected "P2" (diff) +b2.py:7: error: Argument 1 to "f" has incompatible type "D"; expected "P2" b1.py:7: error: Argument 1 to "f" has incompatible type "D"; expected "P1" [case testProtocolsInvalidateByRemovingBase] @@ -11682,3 +11682,57 @@ def f() -> str: ... [out] == main:4: error: Incompatible return value type (got "str | None", expected "int | None") + +[case testGlobalNoneWidenedInFuncWithRedefinition1] +import m +reveal_type(m.x) + +[file m.py] +# mypy: allow-redefinition-new +import m2 + +x = None + +def foo() -> None: + global x + x = m2.bar() + +[file m2.py] +def bar() -> int: return 0 + +[file m2.py.2] +def bar() -> str: return "a" + +[out] +main:2: note: Revealed type is "None | builtins.int" +== +main:2: note: Revealed type is "None | builtins.str" + +[case testGlobalNoneWidenedInFuncWithRedefinition2] +import m +reveal_type(m.x) + +[file m.py] +# mypy: allow-redefinition-new +import m2 + +x = None + +def deco(f): return f + +class C: + @deco + def foo(self) -> None: + global x + x = m2.bar() + +[file m2.py] +def bar() -> int: return 0 + +[file m2.py.2] +def bar() -> str: return "a" + +[out] +main:2: note: Revealed type is "None | builtins.int" +== +main:2: note: Revealed type is "None | builtins.str" From 87b52bce182e721fa54b04ab60476ec085dc36f5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:45:33 +0100 Subject: [PATCH 09/11] Add nested function test case --- test-data/unit/check-redefine2.test | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index b7bf4ee681671..ea357b4628bbe 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -216,6 +216,19 @@ class C: reveal_type(x) # N: Revealed type is "None | builtins.int" +[case testNewRedefineGlobalVariableNoneInit4] +# flags: --allow-redefinition-new --local-partial-types +x = None + +def f() -> None: + def nested() -> None: + global x + x = 1 + + nested() + +reveal_type(x) # N: Revealed type is "None | builtins.int" + [case testNewRedefineGlobalVariableWithUnsupportedType] # flags: --allow-redefinition-new --local-partial-types x = 1 From 5a41b0e994e82a6d2d2d6e6c889c496659b1d01a Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 21 Apr 2026 17:47:34 +0100 Subject: [PATCH 10/11] Minor updates --- mypy/checker.py | 5 +++-- test-data/unit/fine-grained.test | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e063aeb1ab91c..214ac9572beca 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -425,7 +425,7 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi, SplittingVisitor): # NOTE: The names might not be unique, they are only for debugging purposes. widened_vars: list[str] # Global variables widened inside a function body, to be propagated to - # the module-level binder after the function is type checked. + # the module-level binder after the function is type checked (with --allow-redefinition-new). _globals_widened_in_func: list[tuple[NameExpr, Type]] globals: SymbolTable modules: dict[str, MypyFile] @@ -4964,7 +4964,7 @@ def can_widen_in_scope(self, name: NameExpr, orig_type: Type) -> bool: """Can a variable type be widened via assignment in the current scope? Globals can only be widened from within a function if the original type - is None (backward compat with partial type handling of ``x = None``). + is None (backward compat with partial type handling of `x = None`). """ if ( name.kind == GDEF @@ -8372,6 +8372,7 @@ def visit_nonlocal_decl(self, o: NonlocalDecl, /) -> None: def visit_global_decl(self, o: GlobalDecl, /) -> None: if self.options.allow_redefinition_new: + # Add names to binder, since their types could be widened for name in o.names: sym = self.globals.get(name) if sym and isinstance(sym.node, Var) and sym.node.type is not None: diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 88ada8683681f..9a9e4fffe715e 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -11736,3 +11736,29 @@ def bar() -> str: return "a" main:2: note: Revealed type is "None | builtins.int" == main:2: note: Revealed type is "None | builtins.str" + +[case testGlobalNoneWidenedInFuncWithRedefinition3] +import m +reveal_type(m.x) + +[file m.py] +# mypy: allow-redefinition-new +import m2 + +x = None + +def foo() -> None: + def nested(self) -> None: + global x + x = m2.bar() + +[file m2.py] +def bar() -> int: return 0 + +[file m2.py.2] +def bar() -> str: return "a" + +[out] +main:2: note: Revealed type is "None | builtins.int" +== +main:2: note: Revealed type is "None | builtins.str" From 28446013c934042e921738beff3dbeaa18a71817 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 22 Apr 2026 10:17:46 +0100 Subject: [PATCH 11/11] Mention relevant test cases --- mypy/checker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 214ac9572beca..620db7078585c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4965,6 +4965,8 @@ def can_widen_in_scope(self, name: NameExpr, orig_type: Type) -> bool: Globals can only be widened from within a function if the original type is None (backward compat with partial type handling of `x = None`). + + See test cases testNewRedefineGlobalVariableNoneInit[1-4], for example. """ if ( name.kind == GDEF