Issue 1:
Describe the issue:

Doing any kind of binary arithmetic operation (e.g. addition, multiplication) between an uint64 scalar and a signed integer scalar results in values which are float64. Is this really intended? I would expect the unsigned value to be cast to a signed representation prior to the actual operation, with the result then being a signed integer.

A small example is provided in the next section. I stress that this behavior is:

Seen for np.uint64 but not for np.uint32, np.uint16, np.uint8.

Also seen for np.uintp (but that's just an alias to np.uint64, at least on my machine) and for np.ulonglong (which is separate from np.uint64 but also has an itemsize of 8 bytes, at least on my machine).

As the signed integer below, a 1 (Python int) is used. This can be substituted with e.g. np.int8, np.int16, np.int32, np.int64, for the same result.

Reproduce the code example:

import numpy as np

a = np.array([1, 2, 3], dtype=np.uint64)

b = a + 1 # NumPy uint64 array plus signed integer
print(b, b.dtype) # uint64 (OK; no cast to signed expected as arrays keep their dtype)

c = a[0] + 1 # NumPy uint64 scalar plus signed integer
print(c, c.dtype) # float64 (???)

NumPy/Python version information:

1.21.4 3.9.9 (main, Jan 6 2022, 23:52:18)

[GCC 9.3.0]

Issue 2:
Describe the issue:

Expected Behavior:

I expect to create a python Fraction from numpy integer types with no loss of precision

Actual Behavior:

Types are coerced to numpy.float64 and precision is lost

Reproducible code example:

import numpy as np
from fractions import Fraction

nums = np.array([1])
denoms = np.array([18446744073709550592], dtype='uint64')
f = Fraction(nums[0], denoms[0]) # denom has been rounded, is now a float
print(f) # 1.0/1.8446744073709552e+19
print(type(f.denominator)) # <class 'numpy.float64'>

Error message:

No response

NumPy/Python version information:

Numpy: 1.21.2

Python: 3.7.9 (default, Aug 31 2020, 12:42:55) [GCC 7.3.0]

OS: Debian GNU/Linux 10 (buster)

Other information that may be helpful:

I discovered this behavior while creating fractions from parallel arrays representing the numerators and denominators of fractions. This loss of precision is problematic because the float is rounded up so that it is out of range for uint64, despite the original value being valid. In my case I have a workaround, as converting the values to python int before inputting them to the Fraction constructor prevents this issue. However, I wanted to let you know because it is unexpected that an operation with two numpy integer types produced a value with floating point types.

I briefly looked into the Fractions constructor and I discovered that this behavior comes from lines 175-180 of fractions.py:

elif (isinstance(numerator, numbers.Rational) and
isinstance(denominator, numbers.Rational)):
numerator, denominator = (
numerator.numerator * denominator.denominator,
denominator.numerator * numerator.denominator
)

numpy.uint64 is an instance of numbers.Rational:

>>> import numpy as np
>>> import numbers
>>> denoms = np.array([18446744073709550592], dtype='uint64')
>>> isinstance(denoms[0], numbers.Rational)
True

Inspecting the reproducible snippet above with pdb reveals that the multiplication on lines 178-179 of fractions.py is the moment when the type is changed.

Thank you for your time (: