Skip to content

Commit c5394d8

Browse files
committed
Count Any inside type aliases for overload ambiguity
Alias targets store a written Any as TypeOfAny.special_form, so has_any_type ignored Nested = Sequence[Any] and picked the first overload. That contradicted the new docs and the spec (aliases are transparent). Count special-form Any only while expanding aliases. Docs now include an alias example. Tests cover nested and bare Any aliases, including the #15630 identity-check repro.
1 parent 8e2f07b commit c5394d8

4 files changed

Lines changed: 70 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
- Clarify that nested `Any` in an overload argument is ambiguous, the same as a top-level `Any` (PR [21897](https://github.com/python/mypy/pull/21897))
1212

13+
### Other Notable Fixes and Improvements
14+
15+
- Treat type aliases that expand to `Any` (or a type containing `Any`) as ambiguous in overload matching, matching a written `Any` (PR [21897](https://github.com/python/mypy/pull/21897))
16+
1317
## Mypy 2.3
1418

1519
We've just uploaded mypy 2.3.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)).

docs/source/more_types.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,9 @@ for making sure ``summarize`` breaks ties in the same way at runtime.
395395

396396
However, there are two exceptions to the "pick the first match" rule.
397397
First, if multiple variants match because an argument has type ``Any``
398-
or *contains* ``Any`` (for example ``list[Any]`` or ``Sequence[Any]``),
399-
mypy will make the inferred type also be ``Any``:
398+
or *contains* ``Any`` (for example ``list[Any]`` or ``Sequence[Any]``,
399+
including via a type alias), mypy will make the inferred type also be
400+
``Any``:
400401

401402
.. code-block:: python
402403
@@ -412,6 +413,11 @@ mypy will make the inferred type also be ``Any``:
412413
# call is therefore ambiguous in the same way as a top-level Any.
413414
output2_nested = summarize(nested_any)
414415
416+
Nested = list[Any]
417+
aliased: Nested = some_dynamic_function()
418+
# output2_alias is also 'Any': a type alias does not hide the nested Any.
419+
output2_alias = summarize(aliased)
420+
415421
Second, if multiple variants match due to one or more of the arguments
416422
being a union, mypy will make the inferred type be the union of the
417423
matching variant returns:

mypy/checkexpr.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6638,9 +6638,23 @@ class HasAnyType(types.BoolTypeQuery):
66386638
def __init__(self, ignore_in_type_obj: bool) -> None:
66396639
super().__init__(types.ANY_STRATEGY)
66406640
self.ignore_in_type_obj = ignore_in_type_obj
6641+
# Alias targets store a written `Any` as TypeOfAny.special_form.
6642+
# Count those as real Any while expanding aliases, so Nested = Sequence[Any]
6643+
# matches a direct Sequence[Any] for overload ambiguity.
6644+
self.count_alias_special_form_any = False
66416645

66426646
def visit_any(self, t: AnyType) -> bool:
6643-
return t.type_of_any != TypeOfAny.special_form # special forms are not real Any types
6647+
if t.type_of_any == TypeOfAny.special_form:
6648+
return self.count_alias_special_form_any
6649+
return True
6650+
6651+
def visit_type_alias_type(self, t: TypeAliasType) -> bool:
6652+
old = self.count_alias_special_form_any
6653+
self.count_alias_special_form_any = True
6654+
try:
6655+
return super().visit_type_alias_type(t)
6656+
finally:
6657+
self.count_alias_special_form_any = old
66446658

66456659
def visit_callable_type(self, t: CallableType) -> bool:
66466660
if self.ignore_in_type_obj and t.is_type_obj():
@@ -6856,8 +6870,9 @@ def any_causes_overload_ambiguity(
68566870
"""May an argument containing 'Any' cause ambiguous result type on call to overloaded function?
68576871
68586872
This includes nested Any, such as Sequence[Any] or list[Any], not just a
6859-
top-level Any. A nested Any can materialize as different concrete types, so
6860-
if several overloads remain and their return types differ, the result is Any.
6873+
top-level Any, and the same types reached through a type alias. A nested
6874+
Any can materialize as different concrete types, so if several overloads
6875+
remain and their return types differ, the result is Any.
68616876
(If every materialization matched an earlier overload, the spec would drop
68626877
later ones; this helper over-approximates that case as ambiguous.)
68636878

test-data/unit/check-overloading.test

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,10 +2086,9 @@ class Box:
20862086
nested_any: Sequence[Any]
20872087
reveal_type(Box().get(nested_any)) # N: Revealed type is "Any"
20882088

2089-
[case testOverloadNestedAnyViaTypeAliasStillFirstMatch]
2090-
# Type aliases currently hide Any from the overload-ambiguity check because
2091-
# alias targets store Any as a special form (#15630). This is not the #21768
2092-
# rule and is left unchanged; do not "fix" #21768 by assuming aliases agree.
2089+
[case testOverloadNestedAnyViaTypeAliasIsAmbiguous]
2090+
# A type alias must not hide nested Any from overload matching. Alias targets
2091+
# store written Any as a special form; that is still Any (#15630, #21768).
20932092
from typing import Any, Sequence, overload
20942093

20952094
Nested = Sequence[Any]
@@ -2103,7 +2102,43 @@ def f(s): pass
21032102
direct: Sequence[Any]
21042103
aliased: Nested
21052104
reveal_type(f(direct)) # N: Revealed type is "Any"
2106-
reveal_type(f(aliased)) # N: Revealed type is "builtins.int"
2105+
reveal_type(f(aliased)) # N: Revealed type is "Any"
2106+
2107+
[case testOverloadBareAnyTypeAliasIsAmbiguous]
2108+
# https://github.com/python/mypy/issues/15630
2109+
from typing import Any, overload
2110+
2111+
TypeSpec = Any
2112+
2113+
@overload
2114+
def f(x: int) -> int: ...
2115+
@overload
2116+
def f(x: object) -> object: ...
2117+
def f(x): pass
2118+
2119+
a: Any
2120+
b: TypeSpec
2121+
reveal_type(f(a)) # N: Revealed type is "Any"
2122+
reveal_type(f(b)) # N: Revealed type is "Any"
2123+
2124+
[case testOverloadBareAnyTypeAliasDoesNotPickFirstForIdentity]
2125+
# The #15630 reproducer: an alias to Any must not pick the first overload.
2126+
from typing import Any, Union, overload
2127+
2128+
@overload
2129+
def my_origin(x: type) -> type: ...
2130+
@overload
2131+
def my_origin(x: Any) -> Any: ...
2132+
def my_origin(x):
2133+
return x
2134+
2135+
TypeSpec = Any
2136+
2137+
def call_any(spec: Any) -> bool:
2138+
return my_origin(spec) is Union
2139+
2140+
def call_alias(spec: TypeSpec) -> bool:
2141+
return my_origin(spec) is Union
21072142

21082143
[case testOverloadOnOverloadWithType]
21092144
from typing import Any, Type, TypeVar, overload

0 commit comments

Comments
 (0)