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 floats 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.