skip to content

How do math.floor, math.ceil and math.trunc differ on a value like -2.5?

level: middleimportance: should knowfreq 45%

answer

  1. Three of them, three directions
  2. One of the directions depends on the sign
  3. Positive inputs hide the difference
  4. Negative infinity versus zero
  5. Each dispatches to its own dunder

basics

~10 s

math.floor goes toward negative infinity, math.ceil toward positive infinity, and math.trunc toward zero. On -2.5 that gives -3, -2 and -2; on 2.5 floor and trunc agree at 2 while ceil gives 3.

solid answer

~40 s

All three drop the fractional part, but they disagree on direction. `math.floor(x)` moves toward negative infinity, `math.ceil(x)` toward positive infinity, and `math.trunc(x)` toward zero — so `math.floor(-2.5)` is `-3` while `math.trunc(-2.5)` is `-2`, and on positive input floor and trunc agree. Negatives are the only place floor and trunc part company, which is why interviewers pick a negative example. All three return an `int` in Python 3, not a float. None of them is `round()`: `round(-2.5)` is `-2` because it looks for the nearest value and breaks the tie toward the even neighbour. They are protocol calls, delegating to `__floor__`, `__ceil__` and `__trunc__` on the argument's type.

code

python · 7 lines
python
import math

for x in (2.5, -2.5, -0.5):
    print(f"{x:>5}  round={round(x):>3}  floor={math.floor(x):>3}"
          f"  ceil={math.ceil(x):>3}  trunc={math.trunc(x):>3}")

print(type(math.floor(2.5)))   # <class 'int'>

go deeper

for a junior

Recall the three directions — down, up, and toward zero — and be able to give the values for -2.5: -3, -2 and -2. Knowing that positive inputs hide the floor/trunc difference is already a good sign.

for a middle

Explain why truncation is sign-dependent while floor and ceil are not, that all three return an int in Python 3, and that each dispatches to a dunder on the argument's type. Be able to contrast them with round()'s nearest-value semantics.

for a senior

Demonstrate the review instinct: a truncation on a value that can go negative is a latent bucketing bug that positive-only fixtures never catch. Be ready to pick the right operation for binning, pagination and offset arithmetic and to justify it.

for a principal

Own the consistency question across a system: bucket boundaries chosen differently in a service and in the query that reads its output produce silent disagreement. Expect to argue for one documented convention and for test fixtures that always include negative and zero-adjacent values.

## Four operations, four directions Python offers four ways to turn a value with a fractional part into a whole number, and they differ only in which way they go: | call | direction | on 2.5 | on -2.5 | |---|---|---|---| | `math.floor(x)` | toward negative infinity | 2 | -3 | | `math.ceil(x)` | toward positive infinity | 3 | -2 | | `math.trunc(x)` | toward zero | 2 | -2 | | `round(x)` | to the nearest, ties to even | 2 | -2 | On positive input, `floor` and `trunc` are indistinguishable — which is exactly why a bug written with the wrong one survives every test that uses positive numbers, and fires the first time a negative value arrives. Interviewers use `-2.5` for the same reason. ```python import math for x in (2.5, -2.5): print(x, round(x), math.floor(x), math.ceil(x), math.trunc(x)) # 2.5 2 2 3 2 # -2.5 -2 -3 -2 -2 ``` ## Return types In Python 3 all three `math` functions return an `int`, not a float. That is a change from Python 2, where `math.floor(2.5)` gave `2.0`. It matters when the result is used as an index or a count, and it means `math.floor` is a perfectly good way to get an integer floor without any extra conversion. `round()` is the odd one out on typing: with no second argument it returns an `int`, but with `ndigits` it returns the argument's own type. ## Symmetry and asymmetry `floor` and `ceil` are mirror images about zero — `math.ceil(x)` equals `-math.floor(-x)` — so they behave *consistently* on negatives, always stepping the same absolute direction on the number line. `trunc` is the one that changes behaviour with the sign: it steps down for positives and up for negatives, because 'toward zero' is a direction that depends on which side of zero you are on. That asymmetry is what makes truncation a poor choice for anything that must partition a number line evenly, such as bucketing timestamps or coordinates that can go negative. ## They are protocols, not float special cases Each function delegates to a dunder on the argument's type: `math.floor` calls `__floor__`, `math.ceil` calls `__ceil__`, `math.trunc` calls `__trunc__`, and the builtin `round()` calls `__round__`. A type of your own opts in by defining them: ```python import math class Cents: def __init__(self, value): self.value = value def __floor__(self): return Cents(math.floor(self.value)) def __trunc__(self): return int(self.value) def __round__(self, ndigits=None): return Cents(round(self.value, ndigits)) c = Cents(-2.5) print(math.floor(c).value, math.trunc(c), round(c).value) ``` If a type defines only some of them, the others raise `TypeError`. Note also that as of Python 3.12 the `int()` builtin no longer falls back to `__trunc__`; a class that wants to be convertible with `int()` must define `__int__`, and `__index__` is what makes it usable in slicing and as a sequence index. ## Choosing the right one * Bucketing a continuous value into fixed-width bins, where the bins must be uniform across zero — `math.floor`. * Sizing a container to hold everything, such as the number of pages needed for a row count — `math.ceil`. * Dropping a fractional part while preserving the sign's magnitude direction, for instance to display a signed whole-unit part — `math.trunc`. * Reporting the nearest value to a human — `round`. A useful self-check when reviewing code: if a call to `math.trunc` or an implicit truncation appears anywhere the input can be negative, ask whether the author actually meant `math.floor`. That mix-up is one of the most common quiet defects in bucketing and offset arithmetic, and it never shows up in a test suite whose fixtures are all positive.

  • How would you make math.floor and round() work on a class of your own?
    Define the dunders they dispatch to: `__floor__` for `math.floor`, `__ceil__` for `math.ceil`, `__trunc__` for `math.trunc`, and `__round__(self, ndigits=None)` for the builtin `round()`. Each returns whatever your type considers the rounded value. A missing dunder means the corresponding call raises `TypeError`, and since Python 3.12 `int()` no longer falls back to `__trunc__` — define `__int__` for that.
  • Which one do you want for bucketing values that can be negative?
    `math.floor`, because it steps the same direction everywhere on the number line and so produces uniform buckets across zero. `math.trunc` collapses the two buckets adjacent to zero into one, since it rounds up for negatives and down for positives — a defect that positive-only test fixtures never reveal.

Floor and ceiling are fixed storeys in a building — you always go down or always go up. Truncation is walking back to the lobby, which means downstairs if you are above it and upstairs if you are below.

saying these in an interview costs you the question

  • Says math.trunc and math.floor are the same thing
  • Thinks math.floor(-2.5) is -2
  • Believes the math functions return floats in Python 3
  • Confuses math.floor with round()
  • Assumes math.ceil(-2.5) is -3

context