just expanding the comment from Mark Dickinson and to make sure I understand it myself, the CPython round
function is spread over several parts of the code base.
round(number, ndigits)
starts by looking up and invoking the __round__
method on the object. this is implemented by the C function builtin_round_impl
in bltinmodule.c
for float
s this invokes the float.__round__
method, which is implemented in float___round___impl
in floatobject.c:1045 but there's a stub entry point in floatobject.c.h that I think is mostly maintained by Python's argument clinic tool. this header is also where its PyMethodDef
is defined as FLOAT___ROUND___METHODDEF
the C function float___round___impl
starts by checking if ndigits
was not specified (i.e. nothing passed, or passed as None
), in this case then it calls round
from the C standard library (or the version from pymath.c as a fallback).
if ndigits
is specified then it probably calls the version of double_round
in floatobject.c:927. this works in 53bit precision, so adjusts floating point rounding modes and is generally pretty fiddly code, but basically it converts the double to a string with a given precision, and then converts back to a double
for a small number of platforms
there's another version of double_round
at floatobject.c:985 that does the obvious thing of basically round(x * 10**ndigits) / 10**ndigits
, but these extra operations can reduce precision of the result
note that the higher precision version will give different answers to the version in NumPy and equivalent version in R, as commented on here. for example, round(0.075, 2)
results in 0.07 with the builtin round, while numpy and R give 0.08. the easiest way I've found of seeing what's going on is by using the decimal
module to see the full decimal expansion of the float:
from decimal import Decimal
print(Decimal(0.075))
gives: 0.0749999999999999972…
, i.e. 0.075 can't be accurately represented by a (binary) floating point number and the closest number happens to be slightly smaller, and hence it rounds down to 0.07. while the implementation in numpy gives 0.08 because it effectively does round(0.075 * 100) / 100
and the intermediate value happens to round up, i.e:
print(Decimal(0.075 * 100))
giving exactly 7.5
, which rounds exactly to 8
.