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.
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.
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¶
- 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 withjson.loads(text, parse_int=float), and count the IDs that are no longer distinct. JavaScript reads every JSON number the second way. - 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 thefloat()is usually just the lossy part. - Read a column of money amounts from a CSV of yours as
Decimal(s)and asfloat(s), and count the rows whereDecimal(s) == float(s). Before running it, predict which amounts survive. (Only values that are exact binary fractions do: whole amounts,.50,.25,.75.) - Put a column of numbers into a
setas they were parsed, then again after converting every value withfloat(). If the second set is smaller, some distinct numbers in your data became the same float, the way2**53and2**53 + 1do in section 2. - 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 throughto_bytes. Which of the three does the format's specification actually want?
See also¶
- Float equality and NaN: the other half of the same book section, where both sides are floats
bin()is not the bits: the same missing width, seen through a negative number's bits- Making a
bytesobject:bytes([256])raises, and nothing wraps - Comparing two number types ↗ and Casting with
as↗ in the Rust library: the refusal, and the cast that answers it - Comparing signed and unsigned ↗ in the C library:
-1 < 1u, and the usual arithmetic conversions - Machine numbers ↗ in the math library: why the doubles run out of integers at 2^53
- Numeric types ↗ and value comparisons ↗: the widening rule for arithmetic, and the exactness rule for comparison, in Python's own words
- Prompted by §2.3.3 "Comparing numbers" in Tim McNamara's Rust in Action (Manning, 2021). The examples and prose here are written from scratch, and none of the book's listings are reproduced. See RESOURCES.
- A Hash key needs
eql?andhash, not==↗ — the Ruby side: AHashfinds a key by callinghashand theneql? - Ruby has four kinds of equality ↗ — the Ruby side:
equal?is identity,==is value,eql?is value without type conversion (the testHashkeys use), and===… - Set is a core class in 4.0 and keeps insertion order ↗ — the Ruby side: In Ruby 4.0
Setneeds norequireand is written in C,Set[3, 1, 2]iterates as 3, 1, 2, and an element is… - Twenty-three Python traps for a Ruby programmer ↗ — the Ruby side: The twenty-three places where a Ruby reflex gives the wrong answer in Python
1 == 1.0is true, but{1 => :a}[1.0]isnil↗ — the Ruby side: Ruby's==compares an Integer with a Float by value7 / 2is3: integer division floors ↗ — the Ruby side: Ruby's/on two Integers is Python's//