mypy icon indicating copy to clipboard operation
mypy copied to clipboard

Return type of function overload is ignored if parameter has type Any

Open qi55wyqu opened this issue 2 years ago • 6 comments

Bug Report

mypy ignores the return type of a function if the annotation uses @overload and one of the parameters has type Any.

Possible duplicate of #14995.

To Reproduce https://mypy-play.net/?mypy=1.1.1&python=3.11&gist=5ca80f2b256e4d31a8c588b15702b085

@overload
def toInt(value: int) -> int: ...

@overload
def toInt(value: float) -> int: ...

@overload
def toInt(value: Any) -> int | None: ...

def toInt(value: Any) -> int | None:
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


reveal_type(toInt(1))
any_obj: Any = object()
reveal_type(toInt(any_obj))

Expected Behavior

Revealed type is "builtins.int"
Revealed type is "Union[builtins.int, None]"

Actual Behavior Output of mypy

Revealed type is "builtins.int"
Revealed type is "Any"

vs. output of pyright

Type of "toInt(1)" is "int"
Type of "toInt(any_obj)" is "int | None"

Your Environment

  • Mypy version used: 1.1.1
  • Mypy command-line flags: --python-version 3.11
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.11.2

qi55wyqu avatar Apr 05 '23 12:04 qi55wyqu

This is generally expected behavior; we infer Any if multiple overloads match and their return types are different. Perhaps in cases like this (where one of the return types is a subtype of another) it would be OK to infer the wider return type, but in general this is likely to lead to false positives.

JelleZijlstra avatar Apr 05 '23 17:04 JelleZijlstra

Thank you for looking into this.

Unfortunately, I still don't understand, how the return type can change from int | None to Any. I would assume, a function never returns a type that is different from the annotated one.

In that case I would expect an error message such as

Incompatible return value type (got "Any", expected "Optional[int]")

or

Returning Any from function declared to return "Optional[int]"

qi55wyqu avatar Apr 06 '23 09:04 qi55wyqu

If I remove the overloads, the type is correctly inferred as Union[builtins.int, None].

qi55wyqu avatar Apr 06 '23 13:04 qi55wyqu

@JelleZijlstra shouldn't it infer a union of return types of all matched overloads instead of Any?

eltoder avatar Jul 09 '23 17:07 eltoder

shouldn't it infer a union of return types of all matched overloads instead of Any?

That's an option but it is likely annoying in many cases, as inferring a Union generally forces users to do isinstance() checks to narrow down the type.

Consider this example:

from typing import overload, Any

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

def caller1(x: Any):
    return f(x) + 1
def caller2(x: int):
    return f(x) + 1

If f(x) was inferred to return int | str here as you propose, caller1 would produce an error but caller2 would not, which goes against the general principle that annotating something as Any instead of a more precise type should not introduce new errors.

That's why mypy currently infers Any in this sort of case. If we can come up with a heuristic for inferring a more precise type in cases like the OP's, I'd be open to changing the behavior, but I do think returning Any is the right thing in the general case.

JelleZijlstra avatar Jul 09 '23 20:07 JelleZijlstra

That's a good point. Thinking more about it, the OPs case can be solved by a "most precise match" rule. Since he has an explicit overload for Any, we can select it as the best match when the argument type is any and ignore other overloads. This will give the behavior he wants without breaking your example.

eltoder avatar Jul 09 '23 20:07 eltoder