Skip to content

BUG: numpy int64/float64 comparisons are inexact #32071

Open

Description

@sehoffmann

Describe the issue:

Python guarantees that a == b implies hash(a) == hash(b). Around 2**53 this breaks for numpy scalars: == promotes the int64 to float64 (losing precision) so the values compare equal, but each hashes as its own exact value, so the hashes differ.

In the bigger picture, it is also not clear whether np.int64(9007199254740993) == np.float64(9007199254740992.0) should be True in the first place, but I would consider this a separate issue.

Reproduce the code example:

# /// script
# requires-python = ">=3.10"
# dependencies = ["numpy"]
# ///

import numpy as np

a = np.int64(2**53 + 1)  # 9007199254740993 (exact as int64)
b = np.float64(2**53 + 1)  # 9007199254740992.0 (2**53 + 1 is not representable in float64)

print(f"numpy {np.__version__}")
print(f"a = {a!r}")
print(f"b = {b!r}")
print(f"a == b             -> {bool(a == b)}")
print(f"hash(a) == hash(b) -> {hash(a) == hash(b)}")

Error message:

a = np.int64(9007199254740993)
b = np.float64(9007199254740992.0)
a == b             -> True
hash(a) == hash(b) -> False

Python and NumPy Versions:

numpy: 2.5.1
python: 3.12.11

Runtime Environment:

No response

How does this issue affect you or how did you find it:

Found during fixing a hashing related bug in zarr-python (https://github.com/zarr-developers/zarr-python). While fixing, I explored how numpy handles equality tests and hashing of scalar values. np.generic values are part of a bigger structure we need to hash.

Activity

  1. ngoldbaum commented on Jul 23, 2026

    @ngoldbaum
    Member

    Relevant prior art in #2046 and particularly #12525, which collates this whole family of issues. The float64 case was purposely excluded when this generic issue was fixed for NumPy 1.25.

    The basic problem is this:

    >>> a, b = np.int64(2**53 + 1), np.float64(2**53)
    >>> (a, b)
    (np.int64(9007199254740993), np.float64(9007199254740992.0))
    >>> int(a) == float(b)
    False
    >>> # Ok, fine...
    >>> a == b
    np.True_
    >>> # wat
    >>> # hashing uses Python's conception of equality
    >>> len({a, b})
    2

    IMO what we should really do is make integer and float comparisons exact and not touch the hashing behavior. That would make a == b on the above example return np.False_, following Python.

  2. changed the title [-]BUG: numpy int64/float64 scalars can be `==` yet hash differently[/-] [+]BUG: numpy int64/float64 `==` can return `np.True_` for unequal values[/+] on Jul 23, 2026
  3. changed the title [-]BUG: numpy int64/float64 `==` can return `np.True_` for unequal values[/-] [+]BUG: numpy int64/float64 comparisons are inexact[/+] on Jul 23, 2026
  4. ngoldbaum commented on Jul 23, 2026

    @ngoldbaum
    Member

    I changed the title to reflect that the problem is that comparison are inexact.

  5. ngoldbaum commented on Jul 31, 2026

    @ngoldbaum
    Member

    For anyone wanting to work on this: see comments in #32157. This is likely a medium-to-big project and needs research on whether this needs a deprecation.

  6. abdulm5 commented on Jul 31, 2026

    @abdulm5

    Thanks for pointing me toward the history. I read through #12525, #8851, #11801, and PR #23713.

    I could not find a comment from the NumPy 1.25 change that directly explains why float64 was left out. The implementation added exact loops for mixed signed and unsigned integers, while floating-point comparisons continued using normal promotion. My reading of the surrounding discussion is that integer comparisons had a clearly defined exact answer. For integer and float comparisons, it is less clear whether NumPy should compare the original values or first apply its documented common-dtype promotion.

    I agree that this PR changes more behavior than the original hash issue suggests. The NumPy test suite already found a few comparisons that depended on the existing behavior, so downstream code may depend on it too.

    I see two possible approaches to warnings:

    1. We could emit a FutureWarning based on the operand dtypes during type resolution. That should be inexpensive, but it would warn for many comparisons whose result would stay the same.
    2. We could warn only when the future result differs. For arrays, the loop would need to detect both outcomes and carry a mismatch flag back to the ufunc caller so it can emit one warning. I have not found an existing ufunc mechanism that supports this cleanly. The NEP 50 warning path also skips comparison-only precision changes because warnings for them would be noisy.

    Changing the hash behavior does not seem viable. np.int64 still needs to hash like an equal Python int, so some part of the current equality behavior has to change.

    Would you prefer that I look into limiting this PR to NumPy scalar comparisons, which addresses the equality and hash issue directly? The other option is to keep the wider scalar and array implementation as a prototype while we gather compatibility data and plan a transition. @ngoldbaum

  7. ngoldbaum commented on Jul 31, 2026

    @ngoldbaum
    Member

    I think this one will need an opinion from someone else besides me.

  8. abdulm5 commented on Jul 31, 2026

    @abdulm5
  9. seberg commented on Aug 3, 2026

    @seberg
    Member

    I dunno, I guess we can't change the has really either probably? I think if we do this we need to try to keep complexity low.
    In general, it's one of those things where Python goes well beyond normal Programming on grounds of not being compiled, so I feel we should probably discuss this a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions