python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.53k stars 2.83k forks source link

Expression does not infer float as result type #18133

Closed mar10 closed 1 week ago

mar10 commented 1 week ago

Bug Report

The following return line reports an error although the expression returns the correct type.

To Reproduce

def demo_stddev(self) -> float:
    """Return the standard deviation of the time per iteration (aka SD, σ)."""
    values: list[float] = [1.0, 2.0, 3.0, 5.0]
    n = len(values)
    if n <= 1:
        return 0.0
    mean: float = sum(values) / n
    return (sum((x - mean) ** 2 for x in values) / n) ** 0.5

Expected Behavior

Emit no error

Actual Behavior

The following return line reports an error Returning Any from function declared to return "float"

Additional note

Appending # type: ignore is not a solution, because this will interfere with pyright: Pyright correctly accepts the return type and will correctly report an error Unnecessary "# type: ignore" comment

Your Environment

hamdanal commented 1 week ago

You can set enableTypeIgnoreComments: false in your pyright configuration (see here) to tell pyright to leave type: ignore comments to mypy. You would still be able to ignore pyright specific type violations with pyright: ignore comments.

As for the error itself, I believe it is caused by the type declaration of float.__pow__ in typeshed. A negative float raised to the power of another float would result in a complex number, example:

>>> (-1.0)**0.5
(6.123233995736766e-17+1j)

There is no way for typeshed to tell if the result would be float or complex because there is no way to distinguish negative and positive floats so it was decided to annotate the method as returning Any. See the typeshed change here https://github.com/python/typeshed/pull/6287.

brianschubert commented 1 week ago

To add, you can make this type check without a # type: ignore by using math.sqrt instead of ** 0.5:

return math.sqrt(sum((x - mean) ** 2 for x in values) / n)

That also has the benefit of being slightly faster:

$ python3.13 -m timeit -n100 'for x in range(100000): x**0.5'        
100 loops, best of 5: 3.34 msec per loop

$ python3.13 -m timeit -n100 -s 'import math' 'for x in range(100000): math.sqrt(x)'
100 loops, best of 5: 2.25 msec per loop

Alternatively, you could use cast(float, (...) ** 0.5), or disable the no-any-return error code with --no-warn-return-any to match pyright's behavior.

If your actual use case is computing the (population) standard deviation, you might also consider using statistics.pstdev.

mar10 commented 1 week ago

@hamdanal thank you for the explanation. __pow__() indeed returns complex numbers for roots of negative values, so mypy was right. It would have helped, if the method was annotated as returning float | complex, but that's not mypy's fault ;-)

I am aware of the global flags to disable checks. As for the workarounds, I will go with math.sqrt(), thanks for the pointer, @brianschubert