Python round() Function: Syntax, Precision, and Pitfalls
Learn how Python's round() function handles syntax, banker's rounding, floating-point precision limits, and when to use Decimal for exact decimal arithmetic.
Python's round() function is a built-in that returns a number rounded to a specified precision. Understanding round() means knowing not just its syntax but also how it handles ties, floating-point representation limits, and the distinction between rounding and truncation. Its signature is:
round(number, ndigits=None)
When called with one argument, or with None as ndigits, it returns the nearest integer:
round(3.7) # 4 round(3.2) # 3 round(-3.7) # -4 round(3.7, None) # 4
When called on a float with an integer ndigits, it returns a float rounded to that many digits after the decimal point:
round(3.14159, 2) # 3.14 round(2.71828, 3) # 2.718
Negative ndigits rounds to the left of the decimal point:
round(1234, -2) # 1200 round(9876, -3) # 10000
The return type depends on the input. If number is an integer, round() returns an integer whether or not ndigits is given. If number is a float and ndigits is given, it returns a float; if ndigits is omitted or None, it returns an integer:
round(5) # 5 (int) round(5, 2) # 5 (int) round(3.7) # 4 (int) round(3.7, 1) # 3.7 (float)
This distinction matters when you later perform arithmetic that depends on type.
Banker's Rounding: Why round(2.5) Returns 2
The behavior that surprises most developers is how ties are handled. Python uses banker's rounding (also called round-half-to-even). When the fractional part is exactly 0.5, the result is rounded to the nearest even number:
round(2.5) # 2 round(3.5) # 4 round(4.5) # 4 round(5.5) # 6
This is not a bug; it is intentional. For float inputs, it follows IEEE 754's default round-to-nearest-even mode. The rationale is that always rounding 0.5 up introduces a systematic bias in a sequence of operations. Rounding to the nearest even number distributes the bias evenly between rounding up and rounding down.
The same rule applies when ndigits is used. For example, round(2.25, 1) returns 2.2 because 2.25 is exactly halfway between 2.2 and 2.3, and 2 is even.
Precision Issues with Floating-Point Representation
The round() function operates on the binary floating-point representation of the number, not its decimal form. This means that a number like 2.675 is not stored as exactly 2.675 but as the nearest binary fraction. When you call round(2.675, 2), Python rounds the binary value, which is slightly less than 2.675, so the result is 2.67:
>>> round(2.675, 2) 2.67
This is not a bug in round() — it is a consequence of the IEEE 754 double-precision format that Python uses for floats. The same issue affects other languages that use binary floating point, including JavaScript and typical C implementations.
If you need decimal-exact rounding, you must use the decimal module:
from decimal import Decimal, ROUND_HALF_UP Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Decimal('2.68')
Note that the Decimal constructor must receive a string. Passing a float would reintroduce the binary representation error:
Decimal(2.675) # Decimal('2.67499999999999982236431605997495353221893310546875')
When to Use Decimal Instead of round()
The round() function is appropriate when:
- The input is already a float and small precision differences are acceptable.
- You are rounding for display purposes only.
- The numbers involved are not monetary values or other quantities where decimal exactness is required.
Use Decimal when:
- You are working with money, tax rates, or other quantities where the decimal representation is the source of truth.
- You need deterministic rounding that matches decimal arithmetic rules.
- You need to control the rounding mode explicitly (half-up, half-down, ceiling, floor).
from decimal import Decimal, ROUND_HALF_UP price = Decimal("19.99") tax_rate = Decimal("0.08") tax = (price * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # Decimal('1.60')
The Decimal type also lets you choose the rounding mode per operation, which round() cannot do for built-in float values. round() uses banker's rounding for float ties; Decimal offers ROUND_HALF_UP, ROUND_HALF_DOWN, ROUND_CEILING, ROUND_FLOOR, and others.
Rounding Up, Down, and Toward Zero
When you need a specific rounding direction rather than the nearest value, use the math module:
import math math.floor(3.7) # 3 (toward negative infinity) math.ceil(3.2) # 4 (toward positive infinity) math.trunc(-3.7) # -3 (toward zero)
These functions always return integers and do not accept an ndigits argument. To round to a specific number of decimal places with a fixed direction, combine them with multiplication and division:
import math def round_up(value, ndigits=0): factor = 10 ** ndigits return math.ceil(value * factor) / factor round_up(3.14159, 2) # 3.15
This pattern is useful when you need ceiling or floor behavior at a specific precision, which round() cannot provide.
The following table summarizes the behavior of each function for positive and negative inputs:
| Function | Direction | Example (3.7) | Example (-3.7) |
|---|---|---|---|
round() | Nearest, ties to even | 4 | -4 |
math.floor() | Toward negative infinity | 3 | -4 |
math.ceil() | Toward positive infinity | 4 | -3 |
math.trunc() | Toward zero | 3 | -3 |
Negative ndigits and Large Values
As shown earlier, a negative ndigits rounds to a power of ten. The same banker's rounding rule applies:
round(1250, -2) # 1200 (nearest even hundred) round(1350, -2) # 1400 (nearest even hundred)
Note that round(1250, -2) gives 1200 because 1250 is exactly halfway between 1200 and 1300, and the even multiple of 100 is 1200.
Common Mistakes and Edge Cases
One common mistake is assuming round() always returns an integer when ndigits is provided. For a float input, it does not: round(3.7, 0) returns 4.0, not an int. For integer inputs, the type remains int: round(5, 2) returns 5.
Another mistake is using round() to truncate. round(3.9) returns 4, not 3. For truncation, use math.trunc() or int():
int(3.9) # 3 math.trunc(3.9) # 3
A third issue is passing None as ndigits. This is allowed and behaves like omitting the argument:
round(3.7, None) # 4
But passing a non-integer ndigits raises a TypeError:
round(3.7, 0.5) # TypeError: 'float' object cannot be interpreted as an integer
Performance and Maintainability Considerations
round() is a built-in implemented in C, so it is fast for single calls. The performance concern is not the function itself but the surrounding pattern. If you are rounding every value in a large dataset, the cost is dominated by the loop and the float operations, not by round() itself.
The maintainability concern is more significant. Code that relies on round() for financial calculations can produce incorrect results silently because of binary floating-point representation. A developer who sees round(2.675, 2) and expects 2.68 will be confused when the result is 2.67. Documenting the rounding mode and using Decimal for money is a more maintainable approach.
When you need to round many values consistently, consider defining a helper function that makes the rounding mode explicit:
import math def round_half_up(value, ndigits=0): factor = 10 ** ndigits return math.floor(value * factor + 0.5) / factor
This helper implements round-half-up for positive values (the common schoolbook rule) rather than banker's rounding. It is not exact for all floating-point inputs, but it is predictable for most practical cases. For exact decimal behavior, use Decimal.