Why does round(0.5) give 0 in Python?
Short answer
Python rounds halves to the nearest even number, so round(0.5) is 0 and round(2.5) is 2. Float imprecision adds a second surprise, making round(2.675, 2) give 2.67 rather than 2.68.
The short version
Python uses banker's rounding: an exact half rounds to the nearest even number, so round(0.5) is 0 and round(1.5) is 2. This is deliberate, not a bug. A separate surprise comes from float imprecision, which makes round(2.675, 2) return 2.67 rather than the 2.68 you might expect.
Seeing both surprises
round(0.5) # 0
round(1.5) # 2
round(2.5) # 2
round(2.675, 2) # 2.67, not 2.68
The first three show banker's rounding. The last shows floating point: 2.675 cannot be stored exactly, and the stored value is a fraction below 2.675, so it rounds down.
Why banker's rounding exists
Always rounding halves upward introduces a systematic upward bias: over many values, the total drifts high. Rounding halves to the nearest even number sends them up half the time and down half the time, so the bias cancels out across a large set. For statistical and financial aggregation, this is the more defensible rule, which is why it is the standard (and why NumPy and many other systems use it too).
Getting traditional rounding when you need it
If your context requires halves always to go up (many accounting rules do), use the decimal module and state the rule explicitly.
from decimal import Decimal, ROUND_HALF_UP
Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP) # 3
Decimal("2.675").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) # 2.68
Note the strings. Decimal("2.675") is exact, whereas Decimal(2.675) would first build the imprecise float and carry its error in, reproducing the original problem.
The float part is a separate issue
Banker's rounding and float imprecision are two different causes, and it is worth separating them. Even with ROUND_HALF_UP, plain floats would still get 2.675 slightly wrong, because the number was never exactly 2.675 in the first place. Only Decimal built from a string fixes that.
In pandas and NumPy
Series.round and np.round follow the same round-half-to-even rule, so the same surprises appear on columns. If you need different behaviour across a column, apply a Decimal-based function, accepting that it will be slower, and only where the exact rule genuinely matters.
The practical advice
For display, round at the last moment and do not store rounded values, so errors do not accumulate through a pipeline. For money, use Decimal with an explicit rounding rule, because a fraction of a penny multiplied across many rows becomes a real discrepancy. For everything else, Python's default is statistically sound and you can leave it alone, as long as you know why round(0.5) is 0 and are not startled by it.