Skip to content

Comparing an int with a float

Level: 201 · for Python programmers, especially ones coming from Rust or C

One line: Python compares an int with a float by their exact values and never converts one into the other first, so 2**53 + 1 == float(2**53 + 1) is False — where the cast Rust makes you write, and the conversion C does without asking, both answer true.

n = 2**53 + 1
n == float(n)         # False
float(n) == 2**53     # True

Rust will not compile a comparison between an i32 and a u16. rustc 1.98 stops with error[E0308]: mismatched types and suggests b.into(), and the fix Rust in Action gives is to convert one side yourself: pick a type, cast, then compare. C does not refuse; it applies the usual arithmetic conversions and compares whatever comes out. Python has neither the refusal nor the silent conversion. 1 == 1.0 and 7 < 7.5 simply work, and the language reference promises that numbers of the built-in types, Fraction and Decimal compare "without loss of precision", across types as well as within them.

That promise is the part worth learning, because it is exactly what a cast breaks. A double has 53 bits of mantissa, so 2**53 + 1 has no double of its own: float(2**53 + 1) rounds to 2**53. Convert first and two different integers compare equal. Python never converts for a comparison. It compares the exact value of the int with the exact value the float holds, and those differ by one.

The run

Verified output of comparing_int_and_float_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. MIXED NUMBERS COMPARE; A NUMBER AND A STRING DO NOT
     1 == 1.0      True
     7 < 7.5       True
     True == 1     True        bool is a subclass of int
     True + True   2
     1 == '1'      False       == across unrelated types is just False
     1 < '2'       TypeError   < across unrelated types refuses

     Rust will not compile i32 < u16: it refuses to compare two
     integer TYPES. Python has one int type, so that question cannot
     be written here. The refusal it does have is between kinds of
     value: ordering a number against a string.

2. THE COMPARISON IS EXACT
     n = 2**53 + 1 = 9007199254740993

     float(n)                   9007199254740992.0   the nearest double is one less
     n == float(n)              False                compared exactly: they differ by 1
     float(n) == 2**53          True                 and that double IS 2**53
     float(n) == float(2**53)   True                 convert both first, and two ints collide
     n.__eq__(float(n))         NotImplemented       int's == does not know float
     float(n).__eq__(n)         False                float's == does the exact work

     Where the doubles run out, some ints have no double of their own:
     n           float(n)               n == float(n)
     2**53 - 1   9007199254740991.0     True
     2**53       9007199254740992.0     True
     2**53 + 1   9007199254740992.0     False
     2**53 + 2   9007199254740994.0     True
     2**53 + 3   9007199254740996.0     False

3. PAST THE LARGEST DOUBLE, COMPARISON STILL WORKS
     10**400 > 1e308      True
     10**400 < math.inf   True
     float(10**400)       OverflowError   the conversion cannot happen

     The comparison never converted the int, so it has an answer
     where the conversion has none.

4. ARITHMETIC CONVERTS -- ONLY COMPARISON IS EXACT
     n + 0.0         9007199254740992.0   the int became a float first
     n + 0.0 == n    False                adding zero changed the number
     10**400 + 0.0   OverflowError        and past the largest double, it cannot

5. DECIMAL AND FRACTION FOLLOW THE SAME RULE
     The literal 0.1 holds 3602879701896397 / 36028797018963968
     -- (0.1).as_integer_ratio() -- which is not one tenth.

     Decimal('0.1') == 0.1    False       a tenth is not that double
     Decimal('0.1') < 0.1     True        the double is a little MORE than a tenth
     Fraction(1, 10) == 0.1   False
     Fraction(0.1) == 0.1     True        built from the double: the same number
     Decimal(0.1) == 0.1      True        same again, in decimal
     Decimal('0.1') + 0.1     TypeError   compares with a float, refuses to add one
     Fraction(1, 10) + 0.1    0.2         adds, and the answer is a float

6. EQUAL NUMBERS ARE ONE DICT KEY
     len({1, 1.0, True, Fraction(1), Decimal(1)})   1             five types, one number
     {1: 'int', 1.0: 'float', True: 'bool'}         {1: 'bool'}   first key kept, last value
     hash(0.5) == hash(Fraction(1, 2))              True          equal, so they must hash equal
     {n: 'found'}.get(float(n))                     None          float(n) is 2**53, not n
     {2**53: 'found'}.get(float(n))                 'found'

     A dict needs equal keys to hash equal, so hash() is defined on
     the numeric value, whatever type is holding it.

7. A WIDTH HAS TO BE ASKED FOR
     Rust's 300_i32 as i8 is 44: the top bits are dropped. A Python
     int has no width to drop them from, so the width is yours to name:

     (300 + 128) % 256 - 128                 44              arithmetic: wrap into -128..127
     ctypes.c_int8(300).value                44              a C type truncates, as the cast does
     (300).to_bytes(1, 'big', signed=True)   OverflowError   refuses, like i8::try_from
     struct.pack('b', 300)                   struct.error    refuses too
     -1 < 1                                  True            no unsigned type for -1 to become

What the run shows

Section 1: numbers compare with numbers, and a number is not ordered against a string. True == 1 because bool is a subclass of int, not a separate type that converts. The two string rows are the refusal Python 3 does have. 1 == '1' is False, because == between unrelated types falls back to asking whether they are the same object. 1 < '2' raises TypeError, because an ordering has no fallback. Python 2 answered that one, with an arbitrary but consistent order between types; Python 3 turned it into an error. So both languages refuse something. Rust refuses to compare two integer types, and Python, which has one integer type, refuses to order two kinds of value.

Section 2: the comparison is exact, and the work is done by float. n.__eq__(float(n)) is NotImplemented: int does not know how to compare itself with a float, so Python asks the other side, and float.__eq__ compares exact values. The row that matters is float(n) == float(2**53). It is True, and it is the comparison a cast writes: turn the integers into floats, then compare. Two different integers came out equal. The table under it is where that starts. At 2**53 the gap between doubles grows to 2, so from there to 2**54 every odd integer rounds to a neighbour, and n == float(n) is False for exactly those. (2**53 + 3 rounds up to ...996 rather than down to ...994, because a tie goes to the neighbour with an even mantissa.)

Section 3: a comparison can answer where a conversion cannot. 10**400 is past the largest double, and float(10**400) raises OverflowError. Yet 10**400 > 1e308 and 10**400 < math.inf both answer, because the comparison never needed a float for the left side.

Section 4: arithmetic is not exact. Only comparison is. The standard library docs say mixed arithmetic widens the int to float, and it does: n + 0.0 is 9007199254740992.0, so adding zero changed the number, and 10**400 + 0.0 raises the OverflowError that the comparison avoided. The practical version is that x == y can be False while x + 0.0 == y + 0.0 is True. The same values give opposite answers depending on whether a float was involved in the arithmetic before the comparison.

Section 5: Decimal and Fraction follow the same rule. The literal 0.1 holds 3602879701896397 / 36028797018963968, which is not a tenth. So Decimal('0.1') == 0.1 and Fraction(1, 10) == 0.1 are both False, and Decimal('0.1') < 0.1 is True: the double nearest a tenth is slightly more than one. Build the Decimal or Fraction from the float and it holds that exact value, so Fraction(0.1) == 0.1 is True. Decimal compares with a float but will not add one, and raises TypeError. Fraction adds, and the result is a float, which is section 4's widening again.

Section 6: equal numbers are one dictionary key. A dict needs keys that compare equal to hash equal, and Python makes == exact across numeric types. So hash() is defined on the numeric value, and {1, 1.0, True, Fraction(1), Decimal(1)} is a set of one. The same rule works against you in a lookup: {n: 'found'}.get(float(n)) misses, because the key it searches for is 2**53.

Section 7: a width has to be asked for. 300_i32 as i8 is 44 in Rust: the high bits are dropped. A Python int has no high bits to drop, so nothing wraps unless you say how. (300 + 128) % 256 - 128 is the wrap as arithmetic. ctypes.c_int8(300).value gets the same 44 from a C type, which does no overflow check. (300).to_bytes(1, 'big', signed=True) raises OverflowError, which is Python's version of i8::try_from. And -1 < 1 is just True, because there is no unsigned type for -1 to be converted into.

If you are coming from Rust

Rust makes the conversion visible, and Python makes it unnecessary. They are not the same guarantee. a as f64 == b compiles, reads as deliberate, and is wrong past 2^53 in exactly the way section 2 shows. The book resolves the compile error by casting one side to the other's type. That is right for i32 and u16, because every u16 fits in an i32. It is not right for i64 and f64, where the type that looks wider holds fewer exact integers.

Measured 2026-09-16 with rustc 1.98.0 and Apple clang 21.0.0; not verbatim program output
  Rust
  let a: i32 = 10; let b: u16 = 100; a < b      error[E0308]: mismatched types  (help: a < b.into())
  a < b.into()                                  true
  300_i32 as i8                                 44
  i8::try_from(300_i32)                         Err(TryFromIntError(PosOverflow))
  9_007_199_254_740_993_i64 as f64
      == 9_007_199_254_740_992.0                true

  C  (cc -std=c11)
  (double)9007199254740993LL == 9007199254740992.0     1
  long long x = 9007199254740993LL;
  x == 9007199254740992.0                              1   the same conversion, with no cast written
  -1 < 1u                                              0   -1 becomes UINT_MAX before the comparison

The Rust library works through the comparison itself in Comparing two number types ↗, and the cast in Casting with as ↗. Python has no as, so there is no cast to get wrong. There is still float(n), and section 4 shows that arithmetic performs it for you.

If you are coming from C

C has the conversion and hides it. Comparing a long long with a double converts the integer to double first, so the second C row above is true without a cast anywhere in the source. The sharper trap is between signed and unsigned integers: -1 < 1u is false, because -1 becomes UINT_MAX before the comparison happens. The C library's Comparing signed and unsigned ↗ owns that. Python cannot reproduce it, because an int is neither signed nor unsigned; it is a number.

If you are coming from ABAP

ABAP sits nearer C than Python here. When two numeric operands of different types meet in a comparison, ABAP picks a comparison type from a ranking of the numeric types and converts both operands to it. Binary floating point f ranks above the packed and integer types, so an int8 compared with an f is compared as an f, and past 2^53 it loses precision the way the C row does. ABAP will also compare a number with a character field, which Python refuses to order: the text is converted to a number first, and the conversion fails at runtime if it is not one. Check the comparison-type rules in the ABAP keyword documentation for your release before relying on any of this. (Not machine-checked — CI cannot run ABAP.)

Try it

  1. Find a large ID in your own data. Database keys, snowflake IDs and nanosecond timestamps are all bigger than 2^53. Load a JSON file of yours twice, once with json.loads(text) and once with json.loads(text, parse_int=float), and count the IDs that are no longer distinct. JavaScript reads every JSON number the second way.
  2. Search a codebase you maintain for float(. Look for calls sitting next to a == or a <, and ask whether the comparison would still be right without the conversion. Section 2 says Python compares exactly on its own, so the float() is usually just the lossy part.
  3. Read a column of money amounts from a CSV of yours as Decimal(s) and as float(s), and count the rows where Decimal(s) == float(s). Before running it, predict which amounts survive. (Only values that are exact binary fractions do: whole amounts, .50, .25, .75.)
  4. Put a column of numbers into a set as they were parsed, then again after converting every value with float(). If the second set is smaller, some distinct numbers in your data became the same float, the way 2**53 and 2**53 + 1 do in section 2.
  5. Pick a fixed-width field from a binary format you read, like a signed 16-bit sample or an 8-bit status code. Feed it a value that does not fit, once through a mask, once through ctypes, once through to_bytes. Which of the three does the format's specification actually want?

See also