From e0a60eceedd75270b3cd7fc5110324815425f73d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 25 Sep 2026 13:22:37 +0100 Subject: [PATCH 1/3] Limit binder special-casing to imprecise Any --- mypy/binder.py | 17 ++++++++++++++--- mypy/types.py | 14 ++++++++++++++ test-data/unit/check-redefine2.test | 13 ++++++++----- test-data/unit/check-unions.test | 6 ++++-- test-data/unit/check-unreachable-code.test | 12 +++++------- 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/mypy/binder.py b/mypy/binder.py index fb35d96b7ae78..f7e1b59c8b118 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -21,6 +21,7 @@ from mypy.subtypes import is_same_type, is_subtype from mypy.typeops import make_simplified_union from mypy.types import ( + IMPRECISE_ANYS, AnyType, Instance, NoneType, @@ -499,8 +500,7 @@ def assign_type(self, expr: Expression, type: Type, declared_type: Type | None) return p_declared = get_proper_type(declared_type) - p_type = get_proper_type(type) - if isinstance(p_type, AnyType): + if is_imprecise_any(type): # Any type requires some special casing, for both historical reasons, # and to optimise user experience without sacrificing correctness too much. if isinstance(expr, RefExpr) and isinstance(expr.node, Var) and expr.node.is_inferred: @@ -517,7 +517,7 @@ def assign_type(self, expr: Expression, type: Type, declared_type: Type | None) for item in all_items ] self.put(expr, UnionType(new_items)) - elif any(isinstance(get_proper_type(item), AnyType) for item in all_items): + elif any(is_imprecise_any(item) for item in all_items): # Third case: a union already containing Any (most likely from # an un-imported name), in this case we allow assigning Any as well. self.put(expr, type) @@ -708,3 +708,14 @@ def collapse_variadic_union(typ: UnionType) -> Type: else: simplified = TupleType(prefix + [unpack] + suffix, fallback=last.partial_fallback) return UnionType.make_union([simplified] + other_items) + + +def is_imprecise_any(tp: Type) -> bool: + """Is this an imprecise Any type?""" + tp = get_proper_type(tp) + if not isinstance(tp, AnyType): + return False + if tp.type_of_any == TypeOfAny.from_another_any: + assert tp.source_any is not None + tp = tp.source_any + return tp.type_of_any in IMPRECISE_ANYS diff --git a/mypy/types.py b/mypy/types.py index def2f9f207fdf..91f20dd93d55b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -261,6 +261,20 @@ class TypeOfAny: suggestion_engine: Final = 9 +# Some parts of the code distinguish two categories of Any types: +# * Intentional: explicit, from_error, special_form - these are what other +# languages/type-checkers may call "Dynamic", i.e. too tricky to express. +# * Imprecise: unannotated, from_unimported_type, from_omitted_generics - these +# are what other languages/type-checkers may call "Unknown", i.e. values in +# partially annotated legacy code. These are usually "non-sticky", after assigning +# such value to a variable, the variable keeps the original (more precise) type. +IMPRECISE_ANYS: Final = ( + TypeOfAny.unannotated, + TypeOfAny.from_unimported_type, + TypeOfAny.from_omitted_generics, +) + + def deserialize_type(data: JsonDict | str) -> Type: if isinstance(data, str): return Instance.deserialize(data) diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 964a1d02db12b..b5a8aeac0e3da 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -1525,10 +1525,11 @@ def test(x: Union[str, Int]) -> None: reveal_type(y) # N: Revealed type is "Any" [case testNewRedefineNarrowingForNestedUnionWithNone] -# flags: --allow-redefinition -from typing import Any, Union +# flags: --allow-redefinition --ignore-missing-imports +from typing import Union +from somewhere import Unknown -a: Any +a: Unknown Int = Union[int, None] def test(x: Union[str, Int]) -> None: @@ -1552,9 +1553,11 @@ def test(x: Union[str, Int]) -> None: reveal_type(y) # N: Revealed type is "Any" [case testRegularNarrowingForNestedUnionWithNone] -from typing import Any, Union +# flags: --ignore-missing-imports +from typing import Union +from somewhere import Unknown -a: Any +a: Unknown Int = Union[int, None] def test(x: Union[str, Int]) -> None: diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index e7a3ccc1c5123..6394e0722501f 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -524,9 +524,11 @@ def foo(bar: Union[Union[A, B], C]) -> None: [out] [case testAssignAnyToUnion] -from typing import Union, Any +from typing import Union +from somewhere import Unknown # type: ignore + x: Union[int, str] -a: Any +a: Unknown if bool(): x = a reveal_type(x) # N: Revealed type is "builtins.int | builtins.str" diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 11151be1cd693..76f5e0cb0c6bb 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -777,10 +777,8 @@ reveal_type(x) # N: Revealed type is "__main__.B" [typing fixtures/typing-medium.pyi] [case testUnreachableWhenSuperclassIsAny] -from typing import Any +from somewhere import Parent # type: ignore -# This can happen if we're importing a class from a missing module -Parent: Any class Child(Parent): def foo(self) -> int: reveal_type(self) # N: Revealed type is "__main__.Child" @@ -793,11 +791,11 @@ class Child(Parent): def bar(self) -> int: if 1: self = super(Child, self).something() - reveal_type(self) # N: Revealed type is "__main__.Child" + reveal_type(self) # N: Revealed type is "Any" if self is None: - reveal_type(self) - return None - reveal_type(self) # N: Revealed type is "__main__.Child" + reveal_type(self) # N: Revealed type is "None" + return None # E: Incompatible return value type (got "None", expected "int") + reveal_type(self) # N: Revealed type is "Any" return 3 [builtins fixtures/isinstance.pyi] From 683734651a027fa826566e0302c7a0449ae6c0fa Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 25 Sep 2026 22:33:10 +0100 Subject: [PATCH 2/3] Keep reverse logic --- mypy/binder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/binder.py b/mypy/binder.py index f7e1b59c8b118..9984781733cd4 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -517,7 +517,7 @@ def assign_type(self, expr: Expression, type: Type, declared_type: Type | None) for item in all_items ] self.put(expr, UnionType(new_items)) - elif any(is_imprecise_any(item) for item in all_items): + elif any(isinstance(get_proper_type(item), AnyType) for item in all_items): # Third case: a union already containing Any (most likely from # an un-imported name), in this case we allow assigning Any as well. self.put(expr, type) @@ -643,7 +643,7 @@ def get_declaration(expr: BindableExpression) -> Type | None: if isinstance(expr, RefExpr): if isinstance(expr.node, Var): type = expr.node.type - if not isinstance(get_proper_type(type), PartialType): + if not isinstance(type, PartialType): return type elif isinstance(expr.node, TypeInfo): return TypeType(fill_typevars_with_any(expr.node)) From 6aee71352f19c19e662833d75e5b46fb531cf858 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 28 Sep 2026 23:42:36 +0100 Subject: [PATCH 3/3] Add dedicated tests --- test-data/unit/check-narrowing.test | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 29f4cd47929d9..1ee146be86f0e 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -4299,3 +4299,50 @@ def func(y: H) -> H: else: return y [builtins fixtures/primitives.pyi] + +[case testNarrowingToAnyDifferentKinds] +# flags: --ignore-missing-imports +from typing import Any +from whatever import Unimported + +explicit: Any + +def foo(): ... +imprecise = foo() +unimported: Unimported + +x: int +if int(): + x = explicit + reveal_type(x) # N: Revealed type is "Any" + +y: int +if int(): + y = from_error # type: ignore + reveal_type(y) # N: Revealed type is "Any" + +x1: int +if int(): + x1 = imprecise + reveal_type(x1) # N: Revealed type is "builtins.int" + +y1: int +if int(): + y1 = unimported + reveal_type(y) # N: Revealed type is "builtins.int" + +[case testNarrowingToImpreciseAnyFromUnionWithExplicit] +from typing import Any, Union + +def foo(): ... +imprecise = foo() + +x: Union[Any, int] +if int(): + x = imprecise + reveal_type(x) # N: Revealed type is "Any" + +y: Union[str, int] +if int(): + y = imprecise + reveal_type(y) # N: Revealed type is "builtins.str | builtins.int"