mypy icon indicating copy to clipboard operation
mypy copied to clipboard

abs(Union[int, Decimal]) is inferred as object

Open brettcs opened this issue 4 years ago • 7 comments

I'm afraid I'm not sure whether I'm reporting a bug or requesting a feature. I'm happy to leave that up for you all to triage. I'll start with what I'm seeing and what I'd like to see, and then get a little bit into the why.

Here's a minimal reproduction:

from decimal import Decimal
from typing import Union

def check(x: Decimal, y: Union[Decimal, int]) -> bool:
    return x < -abs(y)

With Python 3.7.3 (from Debian buster) and mypy 0.770 (from PyPI), checking this code returns an error:

deccomp.py:6: error: Unsupported operand type for unary - ("object")  [operator]
Found 1 error in 1 file (checked 1 source file)

The return type of abs is inferred as object, I think because that's the common supertype of int and Decimal. It would be nice if, one way or another, the return type of abs could be inferred as some higher numeric type.

The context here is I'm working on accounting software where I want to be careful to do decimal math throughout. In other words, I never want to deal with the decimal.FloatOperation signal. For functions that do basic arithmetic or comparisons across numbers, it's fine to accept arguments that are either int or Decimal, and it's convenient for callers if I can annotate that argument type rather than requiring them to convert their integer arguments to Decimal all the time.

brettcs avatar Mar 29 '20 19:03 brettcs

For context, the definition of abs in typeshed is

def abs(__n: SupportsAbs[_T]) -> _T: ...

Considering the definition of SupportsAbs, this means that abs returns whatever the __abs__ method on its argument type returns. int.__abs__ is declared as returning int in typeshed, and Decimal.__abs__ returns Decimal.

I feel like mypy should be able to use these stubs to infer that abs(Union[int, Decimal]) returns Union[int, Decimal], but the current type inference isn't up to the task. There are probably already some similar issues.

JelleZijlstra avatar Mar 30 '20 04:03 JelleZijlstra

Yeah, I agree that would probably be a better type in this case. But whether to infer a union type or a join is tricky and changing behavior will break code in places; not sure if this one has a fix that would be better.

Fix for your particular issue might be to write a wrapper with a better type.

msullivan avatar Apr 03 '20 23:04 msullivan

I ended up just casting y to Decimal at the top of the function. I think y = Decimal(y) should always work given these types, so the cast let me tell mypy that with less runtime overhead.

brettcs avatar Apr 08 '20 17:04 brettcs

I think the following is a related example:

from typing import Union
from fractions import Fraction
import math

def f(a: Union[Fraction, int]) -> Union[Fraction, int]:
    return math.pi*a

This gives the error on the last line:

Incompatible return value type (got "Union[Any, float]", expected "Union[Fraction, int]")

EDIT: to be clear, there is supposed to be an error, but I feel mypy should infer the type float instead of Union[Any, float]

jvdwetering avatar Apr 21 '20 17:04 jvdwetering

It seems to me that https://github.com/python/typeshed/issues/5275 is related too. There, @erictraut describes:

The problem is that mypy uses a "join" operation to widen types in its type constraint solver rather than using a union operation.

Are there any plans to fix this and change the way the constraint solver works?

PS: This is the first issue that I dug up trough the search that seems to be related and I am new to this project. As was said here, there are probably related issues. Feel free to refer me to a more relevant issue/feature if this has been discussed.

zormit avatar May 03 '21 10:05 zormit

#5392 and #9264 are related, I think

hauntsaninja avatar May 03 '21 18:05 hauntsaninja

Run into this bug today too.

This snippet:

from decimal import Decimal

m = min(Decimal(1), 2)

Decimal(m)

produces:

fiddles/decimal_mypy.py:3: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "object"  [type-var]
fiddles/decimal_mypy.py:5: error: Argument 1 to "Decimal" has incompatible type "object"; expected "Decimal | float | str | tuple[int, Sequence[int], int]"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

using mypy 1.4.1 (compiled: yes) and Python 3.10.9.

I've also read https://github.com/python/typeshed/issues/5275 and it looks that this is fine with Pyright. Relevant quotes from that issue seem to be:

The base problem is that mypy looks for a common base type in case like this, though, and only finds object

and

The problem is that mypy uses a "join" operation to widen types in its type constraint solver rather than using a union operation.

slafs avatar Jun 28 '23 10:06 slafs