math.floor(3.7) returns 3, and so do 3.7 // 1 and int(3.7) - which makes all three look interchangeable. They are not. math.floor(-3.7) returns -4, and int(-3.7) returns -3. Floor rounds down, toward negative infinity. int() truncates toward zero. On positive numbers they agree, which is exactly why this bug survives testing: your test data is positive, production has a negative, and a number is off by one somewhere in a calculation nobody is watching. The three-way distinction between floor, truncation, and rounding is worth ten minutes once.
This is a trivial function with a genuinely non-trivial edge, and the edge only appears when a value goes below zero - which in most codebases is the one case nobody wrote a test for.
Table of contents
- The three operations, side by side
- // is floor division, not integer division
- Where this becomes a real bug
- Floats make floor lie occasionally
- Which one to use
- How this fits the rest of the stack
- FAQ
The three operations, side by side
import math
# floor int() round()
math.floor(3.7) # 3
int(3.7) # 3
round(3.7) # 4
math.floor(-3.7) # -4 <- rounds DOWN
int(-3.7) # -3 <- truncates toward ZERO
round(-3.7) # -4
Stated plainly:
math.floor(x)- the largest integer less than or equal to x. Always moves toward negative infinity.int(x)- discards the fractional part. Always moves toward zero.round(x)- nearest integer, with banker’s rounding on exact halves.math.ceil(x)- the smallest integer greater than or equal to x. Always moves toward positive infinity.
For positive numbers, floor and int are identical. That equivalence is a trap, not a convenience - it means the bug is invisible until it is not.
// is floor division, not integer division
The // operator is commonly called integer division, which is misleading. It is floor division, and it follows math.floor, not int().
7 // 2 # 3
-7 // 2 # -4 <- not -3
int(-7 / 2) # -3 <- true division then truncation
This trips people coming from C, Java, or Go, where integer division truncates toward zero. Python made a deliberate choice, and it is a coherent one: it keeps the identity a == (a // b) * b + (a % b) true for negative operands, which in turn makes the modulo operator behave sensibly.
-7 % 2 # 1 in Python - always has the sign of the divisor
// in C: // -1 in C - sign of the dividend
A Python modulo result is always non-negative for a positive divisor, which is genuinely useful for cyclic indexing - items[i % len(items)] works for negative i in Python and does not in C. The floor-division choice is what buys that.
One more: // on floats returns a float. 7.0 // 2 is 3.0, not 3. If you need an int, wrap it.
Where this becomes a real bug
The pattern is always the same: a calculation that is fine until a value goes negative.
# Pagination offset - fine.
page = offset // per_page
# Time-bucketing with a timezone offset that can go negative.
bucket = (timestamp - epoch) // 3600
# If timestamp < epoch, floor gives you one bucket lower than truncation would.
# Grid coordinates around an origin.
cell_x = math.floor(x / cell_size) # correct for negative x
cell_x = int(x / cell_size) # wrong for negative x - two cells map to 0
That last one is worth staring at. With int(), both -0.5 and 0.5 map to cell 0, so your origin cell is twice the width of every other cell. Every spatial index, every histogram, every tiling scheme has this bug available. It shows up as a subtle asymmetry that nobody notices for a year.
The rule: if the value can be negative and you want consistent bucketing, you want floor, not int. If you genuinely want truncation toward zero - which is rare and usually about display - use int and mean it.
Floats make floor lie occasionally
Not a floor bug, but it surfaces through floor, so it belongs here.
math.floor(0.1 + 0.2 + 0.7) # might not be what you expect
(0.1 + 0.2) == 0.3 # False
Floating point cannot represent 0.1 exactly. Accumulate a few of them and a value that should be exactly 3.0 can be 2.9999999999999996, which floors to 2. The floor is correct; the input was not what you thought.
Where this matters most is money. Never use floats for currency:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # Decimal('0.3'), exactly
# Or work in integer minor units.
cents = 1050 # rather than 10.50
If you are flooring a monetary value computed from float arithmetic, you have two bugs and only one of them is visible.
Which one to use
A short decision list:
- Bucketing, binning, or grid coordinates?
math.flooror//. Consistent behaviour across zero is the whole requirement. - Dividing two integers and want an integer?
//. It is faster thanint(a / b)and avoids a float round-trip that loses precision on large values. - Just want to drop the decimal for display?
int(), and only if the value cannot be negative - or if truncation toward zero is genuinely what you mean. - Want the nearest value?
round(), and remember it uses banker’s rounding:round(0.5)is 0,round(1.5)is 2. - Money?
Decimal, then decide the rounding explicitly.
int(a / b) on large integers is worth calling out. It converts to float first, and floats lose precision beyond 2^53 - so int(10**18 / 3) is silently wrong while 10**18 // 3 is exact.
How this fits the rest of the stack
An off-by-one in a bucketing calculation is not a crash - it is a report that is subtly wrong, a rate limiter that lets through slightly too much, a bill computed a fraction low every time. Those are the bugs that survive longest because nothing alerts on them. The same is true of infrastructure sizing: the cost of being slightly wrong compounds monthly and nothing tells you. The RunxBuild hosting calculator makes the line items explicit - compute, database, storage, bandwidth - so the total is something you checked rather than assumed. The RunxBuild dashboard is where the team sees the real usage.
Useful related references:
- Python Iterate a List: for, enumerate, list comprehension, and map
- Private Methods in Python: There Are None, and That Is Fine
- Build a Python VPN Client: When It Makes Sense and When It Doesn’t
- Python services on RunxBuild
FAQ
What is the difference between floor and int in Python?
math.floor() always rounds down toward negative infinity, while int() truncates toward zero. They give identical results for positive numbers, which is why the difference goes unnoticed. For negatives they diverge: math.floor(-3.7) is -4 and int(-3.7) is -3. If your values can be negative and you want consistent behaviour, use floor.
Does // do floor division or integer division in Python?
Floor division. -7 // 2 is -4, not -3, because it rounds toward negative infinity rather than truncating toward zero. This differs from C, Java, and Go, where integer division truncates. Python’s choice keeps a == (a // b) * b + (a % b) true for negative operands and makes the modulo operator return a result with the divisor’s sign.
Do I need to import math to use floor division?
No. The // operator is built into the language and needs no import. You only need import math for math.floor() and math.ceil() as named functions. For dividing two integers, a // b is the idiomatic choice and is faster than math.floor(a / b) because it avoids the float conversion entirely.
Why does math.floor give the wrong answer sometimes?
It is almost always the input rather than the floor. Floating point cannot represent values like 0.1 exactly, so accumulated arithmetic can produce 2.9999999999999996 where you expected 3.0 - and flooring that correctly gives 2. The floor is right; the arithmetic before it lost precision. For money or anywhere exactness matters, use Decimal or work in integer minor units.
Should I use int(a / b) or a // b in Python?
a // b, especially for large integers. int(a / b) performs true division into a float first, and floats lose precision above 2^53 - so int(10**18 / 3) is silently wrong while 10**18 // 3 is exact. Floor division also avoids the negative-number truncation difference. There is no case where the float round-trip is an advantage.