From 8372d5262c4038a9fd2a04febfc0fc12a6ef2642 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 25 Feb 2026 01:43:23 +0000 Subject: [PATCH 1/8] A test switch to --allow-redefnition-new --- mypy/config_parser.py | 4 +++- mypy/main.py | 10 +++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mypy/config_parser.py b/mypy/config_parser.py index 747981916960d..262aa0c3a7fc2 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -509,7 +509,7 @@ def parse_section( options_key = key # Match aliasing for command line flag. if key.endswith("allow_redefinition"): - options_key += "_old" + options_key += "_new" if key in config_types: ct = config_types[key] elif key in invalid_options: @@ -590,6 +590,8 @@ def parse_section( results["disable_error_code"] = [] if "enable_error_code" not in results: results["enable_error_code"] = [] + if results.get("allow_redefinition_new"): + results["local_partial_types"] = True return results, report_dirs diff --git a/mypy/main.py b/mypy/main.py index acc27f1806b78..179f92c126971 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -97,12 +97,8 @@ def main( stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) ) - if options.allow_redefinition_new and not options.local_partial_types: - fail( - "error: --local-partial-types must be enabled if using --allow-redefinition-new", - stderr, - options, - ) + if options.allow_redefinition_new: + options.local_partial_types = True if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr): # Since --install-types performs user input, we want regular stdout and stderr. @@ -868,7 +864,7 @@ def add_invertible_flag( strict_flag=False, help="Alias to --allow-redefinition-old; will point to --allow-redefinition-new in v2.0", group=strictness_group, - dest="allow_redefinition_old", + dest="allow_redefinition_new", ) add_invertible_flag( From bd2f4204bf224007e3a92d77c1434f3629ed268f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 25 Feb 2026 14:02:47 +0000 Subject: [PATCH 2/8] Play with opposite order for funcitn args --- mypy/checker.py | 18 ++++------------ test-data/unit/check-redefine2.test | 25 ++++++++++++++++++++++ test-data/unit/fixtures/isinstancelist.pyi | 1 + 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 62d74dc254179..61391649f1d30 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4706,15 +4706,8 @@ def infer_rvalue_with_fallback_context( # There are two cases where we want to try re-inferring r.h.s. in a fallback # type context. First case is when redefinitions are allowed, and we got # invalid type when using the preferred (empty) type context. - redefinition_fallback = ( - inferred is not None - and not inferred.is_argument - and not is_valid_inferred_type(rvalue_type, self.options) - ) - # For function arguments the preference order is opposite, and we use errors - # during type-checking as the fallback trigger. - argument_redefinition_fallback = ( - inferred is not None and inferred.is_argument and local_errors.has_new_errors() + redefinition_fallback = inferred is not None and not is_valid_inferred_type( + rvalue_type, self.options ) # Try re-inferring r.h.s. in empty context for union with explicit annotation, # and use it results in a narrower type. This helps with various practical @@ -4726,7 +4719,7 @@ def infer_rvalue_with_fallback_context( ) # Skip literal types, as they have special logic (for better errors). - try_fallback = redefinition_fallback or union_fallback or argument_redefinition_fallback + try_fallback = redefinition_fallback or union_fallback if try_fallback and not is_literal_type_like(rvalue_type): with ( self.msg.filter_errors(save_filtered_errors=True) as alt_local_errors, @@ -4741,7 +4734,6 @@ def infer_rvalue_with_fallback_context( and ( # For redefinition fallback we are fine getting not a subtype. redefinition_fallback - or argument_redefinition_fallback # Skip Any type, since it is special cased in binder. or not isinstance(get_proper_type(alt_rvalue_type), AnyType) and is_proper_subtype(alt_rvalue_type, rvalue_type) @@ -4790,9 +4782,7 @@ def check_simple_assignment( rvalue, type_context=lvalue_type, always_allow_any=always_allow_any ) else: - # Prefer full type context for function arguments as this reduces - # false positives, see issue #19918 for discussion. - if inferred is not None and not inferred.is_argument: + if inferred is not None: preferred = None fallback = lvalue_type else: diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index de9da106716c2..b92c1c2a91a72 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1339,3 +1339,28 @@ def process3(items: list[str]) -> None: reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]" reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]" [builtins fixtures/primitives.pyi] + +[case testNewRedefineHasAttr] +# flags: --allow-redefinition-new --local-partial-types + +def test(lst: list[object]) -> None: + for cls in lst: + if not hasattr(cls, "module"): + break + reveal_type(cls.module) # N: Revealed type is "Any" + +[case testNewRedefineOldAnySpecialCasing] +# flags: --allow-redefinition-new --local-partial-types +from typing import Any + a: Any + x = 1 + if bool(): + x = a + reveal_type(x) # N: Revealed type is "builtins.int" + + if bool(): + y = a + else: + y = 1 + reveal_type(y) # N: Revealed type is "Any" +[builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 2a43606f361a3..54d6e9e38b499 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -18,6 +18,7 @@ Ellipsis = ellipsis() def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass +def hasattr(x: object, name: str) -> bool: pass class int: def __add__(self, x: int) -> int: pass From 8ef8b17bf8c5d30333d07e601674494e35a01691 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 25 Feb 2026 15:18:39 +0000 Subject: [PATCH 3/8] Revert "Play with opposite order for funcitn args" This reverts commit bd2f4204bf224007e3a92d77c1434f3629ed268f. --- mypy/checker.py | 18 ++++++++++++---- test-data/unit/check-redefine2.test | 25 ---------------------- test-data/unit/fixtures/isinstancelist.pyi | 1 - 3 files changed, 14 insertions(+), 30 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 61391649f1d30..62d74dc254179 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4706,8 +4706,15 @@ def infer_rvalue_with_fallback_context( # There are two cases where we want to try re-inferring r.h.s. in a fallback # type context. First case is when redefinitions are allowed, and we got # invalid type when using the preferred (empty) type context. - redefinition_fallback = inferred is not None and not is_valid_inferred_type( - rvalue_type, self.options + redefinition_fallback = ( + inferred is not None + and not inferred.is_argument + and not is_valid_inferred_type(rvalue_type, self.options) + ) + # For function arguments the preference order is opposite, and we use errors + # during type-checking as the fallback trigger. + argument_redefinition_fallback = ( + inferred is not None and inferred.is_argument and local_errors.has_new_errors() ) # Try re-inferring r.h.s. in empty context for union with explicit annotation, # and use it results in a narrower type. This helps with various practical @@ -4719,7 +4726,7 @@ def infer_rvalue_with_fallback_context( ) # Skip literal types, as they have special logic (for better errors). - try_fallback = redefinition_fallback or union_fallback + try_fallback = redefinition_fallback or union_fallback or argument_redefinition_fallback if try_fallback and not is_literal_type_like(rvalue_type): with ( self.msg.filter_errors(save_filtered_errors=True) as alt_local_errors, @@ -4734,6 +4741,7 @@ def infer_rvalue_with_fallback_context( and ( # For redefinition fallback we are fine getting not a subtype. redefinition_fallback + or argument_redefinition_fallback # Skip Any type, since it is special cased in binder. or not isinstance(get_proper_type(alt_rvalue_type), AnyType) and is_proper_subtype(alt_rvalue_type, rvalue_type) @@ -4782,7 +4790,9 @@ def check_simple_assignment( rvalue, type_context=lvalue_type, always_allow_any=always_allow_any ) else: - if inferred is not None: + # Prefer full type context for function arguments as this reduces + # false positives, see issue #19918 for discussion. + if inferred is not None and not inferred.is_argument: preferred = None fallback = lvalue_type else: diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index b92c1c2a91a72..de9da106716c2 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1339,28 +1339,3 @@ def process3(items: list[str]) -> None: reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]" reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]" [builtins fixtures/primitives.pyi] - -[case testNewRedefineHasAttr] -# flags: --allow-redefinition-new --local-partial-types - -def test(lst: list[object]) -> None: - for cls in lst: - if not hasattr(cls, "module"): - break - reveal_type(cls.module) # N: Revealed type is "Any" - -[case testNewRedefineOldAnySpecialCasing] -# flags: --allow-redefinition-new --local-partial-types -from typing import Any - a: Any - x = 1 - if bool(): - x = a - reveal_type(x) # N: Revealed type is "builtins.int" - - if bool(): - y = a - else: - y = 1 - reveal_type(y) # N: Revealed type is "Any" -[builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 54d6e9e38b499..2a43606f361a3 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -18,7 +18,6 @@ Ellipsis = ellipsis() def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass -def hasattr(x: object, name: str) -> bool: pass class int: def __add__(self, x: int) -> int: pass From d72d98d7f6cbf329b0f09891df476b4a771d0473 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 25 Feb 2026 16:07:09 +0000 Subject: [PATCH 4/8] Add test case for another edge case --- mypy/checker.py | 7 ++-- test-data/unit/check-redefine2.test | 39 ++++++++++++++++++++++ test-data/unit/fixtures/isinstancelist.pyi | 1 + 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 62d74dc254179..0d886df321c87 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4720,7 +4720,7 @@ def infer_rvalue_with_fallback_context( # and use it results in a narrower type. This helps with various practical # examples, see e.g. testOptionalTypeNarrowedByGenericCall. union_fallback = ( - inferred is None + preferred_context is not None and isinstance(get_proper_type(lvalue_type), UnionType) and binder_version == self.binder.version ) @@ -4739,9 +4739,8 @@ def infer_rvalue_with_fallback_context( not alt_local_errors.has_new_errors() and is_valid_inferred_type(alt_rvalue_type, self.options) and ( - # For redefinition fallback we are fine getting not a subtype. - redefinition_fallback - or argument_redefinition_fallback + # For redefinition fallbacks we are fine getting not a subtype. + inferred is not None # Skip Any type, since it is special cased in binder. or not isinstance(get_proper_type(alt_rvalue_type), AnyType) and is_proper_subtype(alt_rvalue_type, rvalue_type) diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index de9da106716c2..803e98ac70877 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1339,3 +1339,42 @@ def process3(items: list[str]) -> None: reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]" reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]" [builtins fixtures/primitives.pyi] + +[case testNewRedefineHasAttr] +# flags: --allow-redefinition-new --local-partial-types + +def test(lst: list[object]) -> None: + for cls in lst: + if not hasattr(cls, "module"): + break + reveal_type(cls.module) # N: Revealed type is "Any" +[builtins fixtures/isinstancelist.pyi] + +[case testNewRedefineOldAnySpecialCasing] +# flags: --allow-redefinition-new --local-partial-types +from typing import Any + +def test() -> None: + a: Any + x = 1 + if bool(): + x = a + reveal_type(x) # N: Revealed type is "builtins.int" + + if bool(): + y = a + else: + y = 1 + reveal_type(y) # N: Revealed type is "Any" + +[case testNewRedefineUnionArgumentFallback] +# flags: --allow-redefinition-new --local-partial-types +from typing import Optional, TypeVar + +T = TypeVar("T") +async def gen(x: T) -> T: ... + +async def test(x: Optional[str]) -> None: + if x is None: + x = await gen("foo") + reveal_type(x) # N: Revealed type is "builtins.str" diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 2a43606f361a3..54d6e9e38b499 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -18,6 +18,7 @@ Ellipsis = ellipsis() def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass +def hasattr(x: object, name: str) -> bool: pass class int: def __add__(self, x: int) -> int: pass From 0649efeb59eb41318a8c38c9b7a22dd143997517 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 25 Feb 2026 17:02:26 +0000 Subject: [PATCH 5/8] Play a bit more while we are here --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0d886df321c87..7326aacd065cf 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4740,7 +4740,8 @@ def infer_rvalue_with_fallback_context( and is_valid_inferred_type(alt_rvalue_type, self.options) and ( # For redefinition fallbacks we are fine getting not a subtype. - inferred is not None + redefinition_fallback + or argument_redefinition_fallback # Skip Any type, since it is special cased in binder. or not isinstance(get_proper_type(alt_rvalue_type), AnyType) and is_proper_subtype(alt_rvalue_type, rvalue_type) From 182eba945c143af4dc2c6c8275c0a8bd2de954ef Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 25 Feb 2026 19:30:14 +0000 Subject: [PATCH 6/8] Handle another edge case --- mypy/binder.py | 5 ++++- mypy/checker.py | 3 +++ test-data/unit/check-redefine2.test | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mypy/binder.py b/mypy/binder.py index 4de939e27501b..24b2c544c1153 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -318,7 +318,10 @@ def update_from_options(self, frames: list[Frame]) -> bool: # variable types to be widened using subsequent assignments. This is # tricky to support for instance attributes (primarily due to deferrals), # so we don't use it for them. - old_semantics = not self.bind_all or extract_var_from_literal_hash(key) is None + var = extract_var_from_literal_hash(key) + old_semantics = ( + not self.bind_all or var is None or not var.is_inferred and not var.is_argument + ) if old_semantics and any(x is None for x in resulting_values): # We didn't know anything about key before # (current_value must be None), and we still don't diff --git a/mypy/checker.py b/mypy/checker.py index 7326aacd065cf..ab5b6a291151a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3427,6 +3427,9 @@ def check_assignment( self.options.allow_redefinition_new and lvalue_type is not None and not isinstance(lvalue_type, PartialType) + and isinstance(lvalue, NameExpr) + and isinstance(lvalue.node, Var) + and lvalue.node.is_inferred ): # TODO: Can we use put() here? self.binder.assign_type(lvalue, lvalue_type, lvalue_type) diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 803e98ac70877..180b5716d07d9 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -69,7 +69,7 @@ def f1() -> None: x: Union[int, str] = 0 reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" x = "" - reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" [case testNewRedefineUninitializedCodePath3] # flags: --allow-redefinition-new --local-partial-types @@ -1378,3 +1378,20 @@ async def test(x: Optional[str]) -> None: if x is None: x = await gen("foo") reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineNarrowingOnFirstAssignment] +# flags: --allow-redefinition-new --local-partial-types +from typing import Any + +li: list[int] + +def test() -> None: + x: list[Any] = li + reveal_type(x) # N: Revealed type is "builtins.list[Any]" + + if bool(): + y: list[Any] = li + else: + y = li + reveal_type(y) # N: Revealed type is "builtins.list[Any]" +[builtins fixtures/primitives.pyi] From 23605526bfa35f2360ccb03d4fedee7c9424d31c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 26 Feb 2026 01:23:49 +0000 Subject: [PATCH 7/8] Try some real madness --- mypy/semanal_typeargs.py | 6 ++++++ test-data/unit/check-redefine2.test | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 0f62a4aa8b1a2..abd4c1786b936 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -33,8 +33,10 @@ TypeVarTupleType, TypeVarType, UnboundType, + UnionType, UnpackType, flatten_nested_tuples, + flatten_nested_unions, get_proper_type, get_proper_types, split_with_prefix_and_suffix, @@ -118,6 +120,10 @@ def visit_tuple_type(self, t: TupleType) -> None: # and we need to return an Instance instead of TupleType. super().visit_tuple_type(t) + def visit_union_type(self, t: UnionType) -> None: + super().visit_union_type(t) + t.items = flatten_nested_unions(t.items) + def visit_callable_type(self, t: CallableType) -> None: super().visit_callable_type(t) t.normalize_trivial_unpack() diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 180b5716d07d9..8e4200b79dc2c 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1395,3 +1395,30 @@ def test() -> None: y = li reveal_type(y) # N: Revealed type is "builtins.list[Any]" [builtins fixtures/primitives.pyi] + +[case testNewRedefineNarrowingForNestedUnionWithAny] +# flags: --allow-redefinition-new --local-partial-types +from typing import Any, Union + +a: Any +Int = Union[int, Any] + +def test(x: Union[str, Int]) -> None: + x = a + reveal_type(x) # N: Revealed type is "Any" + +[case testNewRedefineWidenedArgumentDeferral] +# flags: --allow-redefinition-new --local-partial-types +def foo(x: int) -> None: + reveal_type(x) # N: Revealed type is "builtins.int" + if bool(): + x = "no" + c: C + c.x + reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" + +class C: + def __init__(self) -> None: + self.x = defer() + +def defer() -> int: ... From a66e0bacfca02343227edd34c1b21a93ef66174b Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 26 Feb 2026 09:22:36 +0000 Subject: [PATCH 8/8] I just want to see the scope of the fallout --- mypy/semanal_typeargs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index abd4c1786b936..1de25f8fe34b3 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -122,7 +122,7 @@ def visit_tuple_type(self, t: TupleType) -> None: def visit_union_type(self, t: UnionType) -> None: super().visit_union_type(t) - t.items = flatten_nested_unions(t.items) + t.items = flatten_nested_unions(t.items, handle_recursive=False) def visit_callable_type(self, t: CallableType) -> None: super().visit_callable_type(t)