Python round() Function: Syntax, Precision, and Pitfalls
python round function: Understand Python's round() function: syntax, banker's rounding, floating-point precision limits, and when to use Decimal for exact decimal arit...
Python's round() function is a built-in that returns a number rounded to a specified precision. Understanding the python round function 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, it returns the nearest integer:
round(3.7) # 4 round(3.2) # 3 round(-3.7) # -4
When called with ndigits, it returns a float with that many digits after the decimal point:
round(3.14159, 2) # 3.14 round(2.71828, 3) # 2.718
The second argument can be negative, which rounds to powers of ten:
round(1234, -2) # 1200 round(9876, -3) # 10000
The return type depends on the input. If number is an integer and ndigits is omitted, the result is an integer. If ndigits is provided, the result is a float even when the input is an integer:
round(5) # 5 (int) round(5, 2) # 5.0 (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 and specified by the IEEE 754 standard. 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 any language that uses binary floating point, including JavaScript and C.
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. round() always uses banker's rounding for 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
The ndigits argument can be negative, which rounds to the left of the decimal point:
round(1234, -1) # 1230 round(1234, -2) # 1200 round(9876, -3) # 10000
This is useful when you need to round population counts, file sizes, or other quantities to a convenient magnitude. The same banker's rounding rule applies:
round(1250, -2) # 1200 (because 2 is even) round(1350, -2) # 1400 (because 4 is even)
Note that round(1250, -2) gives 1200 because 1250 is exactly halfway between 1200 and 1300, and the even result is 1200.
Common Mistakes and Edge Cases
One common mistake is assuming round() returns an integer when ndigits is provided. It does not — it returns a float:
round(5, 2) # 5.0 (float, not int)
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:
def round_half_up(value, ndigits=0): factor = 10 ** ndigits return math.floor(value * factor + 0.5) / factor
This helper implements round-half-up (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.