Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

- No longer provide mypyc-accelerated wheels for macOS x86_64 [mypyc-wheels #119](https://github.com/mypyc/mypy_mypyc-wheels/pull/119)

### Documentation Updates

- 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))

### Other Notable Fixes and Improvements

- 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))

## Mypy 2.3

We've just uploaded mypy 2.3.0 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)).
Expand Down
18 changes: 16 additions & 2 deletions docs/source/more_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,10 @@ will have an inferred type of ``float``. The implementer is responsible
for making sure ``summarize`` breaks ties in the same way at runtime.

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

.. code-block:: python

Expand All @@ -404,6 +406,18 @@ First, if multiple variants match due to an argument being of type
# output2 is of type 'Any'
output2 = summarize(dynamic_var)

nested_any: list[Any] = some_dynamic_function()

# output2_nested is also 'Any'. A list[Any] is not "really" a list[int]:
# it can hold strings, and list[str] selects the other overload. The
# call is therefore ambiguous in the same way as a top-level Any.
output2_nested = summarize(nested_any)

Nested = list[Any]
aliased: Nested = some_dynamic_function()
# output2_alias is also 'Any': a type alias does not hide the nested Any.
output2_alias = summarize(aliased)

Second, if multiple variants match due to one or more of the arguments
being a union, mypy will make the inferred type be the union of the
matching variant returns:
Expand Down
23 changes: 22 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6638,9 +6638,23 @@ class HasAnyType(types.BoolTypeQuery):
def __init__(self, ignore_in_type_obj: bool) -> None:
super().__init__(types.ANY_STRATEGY)
self.ignore_in_type_obj = ignore_in_type_obj
# Alias targets store a written `Any` as TypeOfAny.special_form.
# Count those as real Any while expanding aliases, so Nested = Sequence[Any]
# matches a direct Sequence[Any] for overload ambiguity.
self.count_alias_special_form_any = False

def visit_any(self, t: AnyType) -> bool:
return t.type_of_any != TypeOfAny.special_form # special forms are not real Any types
if t.type_of_any == TypeOfAny.special_form:
return self.count_alias_special_form_any
return True

def visit_type_alias_type(self, t: TypeAliasType) -> bool:
old = self.count_alias_special_form_any
self.count_alias_special_form_any = True
try:
return super().visit_type_alias_type(t)
finally:
self.count_alias_special_form_any = old

def visit_callable_type(self, t: CallableType) -> bool:
if self.ignore_in_type_obj and t.is_type_obj():
Expand Down Expand Up @@ -6855,6 +6869,13 @@ def any_causes_overload_ambiguity(
) -> bool:
"""May an argument containing 'Any' cause ambiguous result type on call to overloaded function?

This includes nested Any, such as Sequence[Any] or list[Any], not just a
top-level Any, and the same types reached through a type alias. A nested
Any can materialize as different concrete types, so if several overloads
remain and their return types differ, the result is Any.
(If every materialization matched an earlier overload, the spec would drop
later ones; this helper over-approximates that case as ambiguous.)

Note that this sometimes returns True even if there is no ambiguity, since a correct
implementation would be complex (and the call would be imprecisely typed due to Any
types anyway).
Expand Down
234 changes: 234 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,240 @@ a: Any
reveal_type(f(a)) # N: Revealed type is "def (*Any, **Any) -> Any"
reveal_type(f(a)(a)) # N: Revealed type is "Any"

[case testOverloadNestedAnyInGenericArgumentIsAmbiguous]
# https://github.com/python/mypy/issues/21768
# Nested Any is treated like top-level Any for overload matching.
# Sequence[Any] can materialize as Sequence[int] (first overload) or as
# another sequence type (only the object overload), so the result is Any.
from typing import Any, Mapping, Sequence, overload

@overload
def f(s: Sequence[int]) -> int: ...
@overload
def f(s: object) -> object: ...
def f(s): pass

concrete: Sequence[int]
nested_any: Sequence[Any]
top_level_any: Any
reveal_type(f(concrete)) # N: Revealed type is "builtins.int"
reveal_type(f(nested_any)) # N: Revealed type is "Any"
reveal_type(f(top_level_any)) # N: Revealed type is "Any"

@overload
def g(s: Mapping[str, int]) -> int: ...
@overload
def g(s: object) -> object: ...
def g(s): pass

mapping_any: Mapping[str, Any]
reveal_type(g(mapping_any)) # N: Revealed type is "Any"

[case testOverloadNestedAnyUnambiguousWhenReturnsMatch]
# If remaining overloads agree on the return type, nested Any is not ambiguous.
from typing import Any, Sequence, overload

@overload
def f(s: Sequence[int]) -> str: ...
@overload
def f(s: object) -> str: ...
def f(s): pass

nested_any: Sequence[Any]
reveal_type(f(nested_any)) # N: Revealed type is "builtins.str"

[case testOverloadNestedAnyDoesNotForceAnyWhenOtherArgSelects]
# Nested Any does not collapse the result when it does not itself cause
# multiple overloads with differing returns to remain.
from typing import Any, Sequence, overload

@overload
def f(x: Sequence[int], y: int) -> int: ...
@overload
def f(x: Sequence[int], y: str) -> str: ...
def f(x, y): pass

nested_any: Sequence[Any]
reveal_type(f(nested_any, 1)) # N: Revealed type is "builtins.int"
reveal_type(f(nested_any, "x")) # N: Revealed type is "builtins.str"
a: Any
reveal_type(f(nested_any, a)) # N: Revealed type is "Any"

[case testOverloadNestedAnyWithIncompatibleOverlappingReturns]
# Issue 21768's original pair also has unsafely overlapping returns.
# The overlap error is separate; the call-site result is still Any.
from typing import Any, Sequence, overload

@overload
def f(s: Sequence[int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f(s: object) -> int: ...
def f(s): pass

concrete: Sequence[int]
nested_any: Sequence[Any]
reveal_type(f(concrete)) # N: Revealed type is "builtins.str"
reveal_type(f(nested_any)) # N: Revealed type is "Any"

[case testOverloadNestedAnyInhabitationCounterexample]
# Picking the Sequence[int] overload for Sequence[Any] is unsound:
# a Sequence[str] inhabits Sequence[Any], and that call takes the object
# overload (int), not str. See typing spec overload call evaluation step 5.
from typing import Any, Sequence, overload

@overload
def f(s: Sequence[int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def f(s: object) -> int: ...
def f(s): pass

as_str: Sequence[str]
nested_any: Sequence[Any] = as_str
reveal_type(f(as_str)) # N: Revealed type is "builtins.int"
reveal_type(f(nested_any)) # N: Revealed type is "Any"

[case testOverloadNestedAnyOnlyOneOverloadMatches]
# Nested Any is not a blunt "always Any" switch. If only one overload
# is compatible, that overload wins even when the argument contains Any.
from typing import Any, Sequence, overload

@overload
def f(s: Sequence[int]) -> str: ...
@overload
def f(s: str) -> int: ...
def f(s): pass

nested_any: Sequence[Any]
reveal_type(f(nested_any)) # N: Revealed type is "builtins.str"
reveal_type(f("x")) # N: Revealed type is "builtins.int"

[case testOverloadNestedAnySpecExampleThreeOverloads]
# Spec step-5 example: every materialization of list[Any] matches the
# first or second overload, so the Any fallback is dropped. The first
# two remain with different returns, so the call is still Any.
# Picking the first overload here would be the #21768 "fix" and is wrong.
from typing import Any, overload

@overload
def example(x: list[int]) -> int: ...
@overload
def example(x: list[Any]) -> str: ...
@overload
def example(x: Any) -> bytes: ...
def example(x): pass

a: list[Any]
reveal_type(example(a)) # N: Revealed type is "Any"

[case testOverloadNestedAnyMatchesExistingListMatrix]
# Already locked in by testOverloadsUsingAny: invariant list[int]/list[str]
# with a list[Any] argument is Any. Repeated here next to the #21768 cases
# so that matrix cannot drift from the Sequence[Any] rule.
from typing import Any, List, overload

@overload
def foo(x: List[int]) -> int: ...
@overload
def foo(x: List[str]) -> str: ...
def foo(x): pass

c: List[Any]
reveal_type(foo(c)) # N: Revealed type is "Any"
[builtins fixtures/list.pyi]

[case testOverloadNestedAnyInDictValues]
from typing import Any, overload

@overload
def f(s: dict[str, int]) -> int: ...
@overload
def f(s: object) -> object: ...
def f(s): pass

d: dict[str, Any]
reveal_type(f(d)) # N: Revealed type is "Any"
[builtins fixtures/dict.pyi]

[case testOverloadNestedAnyInFixedTuple]
from typing import Any, overload

@overload
def g(s: tuple[int, int]) -> str: ... # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
@overload
def g(s: object) -> int: ...
def g(s): pass

t: tuple[Any, int]
reveal_type(g(t)) # N: Revealed type is "Any"
[builtins fixtures/tuple.pyi]

[case testOverloadNestedAnyOnMethod]
from typing import Any, Sequence, overload

class Box:
@overload
def get(self, key: Sequence[int]) -> int: ...
@overload
def get(self, key: object) -> object: ...
def get(self, key): pass

nested_any: Sequence[Any]
reveal_type(Box().get(nested_any)) # N: Revealed type is "Any"

[case testOverloadNestedAnyViaTypeAliasIsAmbiguous]
# A type alias must not hide nested Any from overload matching. Alias targets
# store written Any as a special form; that is still Any (#15630, #21768).
from typing import Any, Sequence, overload

Nested = Sequence[Any]

@overload
def f(s: Sequence[int]) -> int: ...
@overload
def f(s: object) -> object: ...
def f(s): pass

direct: Sequence[Any]
aliased: Nested
reveal_type(f(direct)) # N: Revealed type is "Any"
reveal_type(f(aliased)) # N: Revealed type is "Any"

[case testOverloadBareAnyTypeAliasIsAmbiguous]
# https://github.com/python/mypy/issues/15630
from typing import Any, overload

TypeSpec = Any

@overload
def f(x: int) -> int: ...
@overload
def f(x: object) -> object: ...
def f(x): pass

a: Any
b: TypeSpec
reveal_type(f(a)) # N: Revealed type is "Any"
reveal_type(f(b)) # N: Revealed type is "Any"

[case testOverloadBareAnyTypeAliasDoesNotPickFirstForIdentity]
# The #15630 reproducer: an alias to Any must not pick the first overload.
from typing import Any, Union, overload

@overload
def my_origin(x: type) -> type: ...
@overload
def my_origin(x: Any) -> Any: ...
def my_origin(x):
return x

TypeSpec = Any

def call_any(spec: Any) -> bool:
return my_origin(spec) is Union

def call_alias(spec: TypeSpec) -> bool:
return my_origin(spec) is Union

[case testOverloadOnOverloadWithType]
from typing import Any, Type, TypeVar, overload
from mod import MyInt
Expand Down
36 changes: 36 additions & 0 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,42 @@ def print_custom_table() -> None:
[out]
_testLoadsOfOverloads.py:24: note: Revealed type is "str"

[case testOverloadNestedAnyWithFullStubs]
# Full-stub counterpart of #21768: collections.abc.Sequence, dict, tuple.
from collections.abc import Sequence
from typing import Any, overload

@overload
def f(s: Sequence[int]) -> int: ...
@overload
def f(s: object) -> object: ...
def f(s: object) -> object:
return 0

concrete: Sequence[int] = [1]
nested: Sequence[Any] = [1]
as_str: Sequence[str] = ["x"]
print(f(concrete), f(nested), f(as_str))
reveal_type(f(concrete))
reveal_type(f(nested))
reveal_type(f(as_str))

@overload
def g(s: dict[str, int]) -> int: ...
@overload
def g(s: object) -> object: ...
def g(s: object) -> object:
return 0

d: dict[str, Any] = {"a": 1}
print(g(d))
reveal_type(g(d))
[out]
_testOverloadNestedAnyWithFullStubs.py:16: note: Revealed type is "int"
_testOverloadNestedAnyWithFullStubs.py:17: note: Revealed type is "Any"
_testOverloadNestedAnyWithFullStubs.py:18: note: Revealed type is "object"
_testOverloadNestedAnyWithFullStubs.py:29: note: Revealed type is "Any"

[case testReduceWithAnyInstance]
from typing import Iterable
from functools import reduce
Expand Down
Loading