CPython's float Implementation in C
The PyFloatObject struct, the ob_fval double, the freelist, arithmetic via CONVERT_TO_DOUBLE, hashing with _Py_HashDouble, repr/str through PyOS_double_to_string, conversions, and nan/inf handling traced through the CPython 3.15 source.
Python’s float is a thin wrapper around a C double. In CPython that is
implemented by PyFloatObject in Objects/floatobject.c, and almost every
operation reduces to doing C double arithmetic and boxing the result back
with PyFloat_FromDouble. This article traces the representation and the hot
paths through the 3.15 source.
The object struct
// Include/cpython/floatobject.h
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;There is exactly one payload field, ob_fval, a native IEEE-754 double.
There is no digit array, no tag word and no separate allocation: the whole
object is sizeof(PyFloatObject) bytes. PyFloat_AS_DOUBLE(op) reads
ob_fval directly without any type check.
Allocation
PyFloat_FromDouble is the single constructor used everywhere:
// Objects/floatobject.c
PyObject *
PyFloat_FromDouble(double fval)
{
PyFloatObject *op = _Py_FREELIST_POP(PyFloatObject, floats);
if (op == NULL) {
op = PyObject_Malloc(sizeof(PyFloatObject));
if (!op) {
return PyErr_NoMemory();
}
_PyObject_Init((PyObject*)op, &PyFloat_Type);
}
op->ob_fval = fval;
return (PyObject *) op;
}It first tries to pop a recycled object from the float freelist (up to
Py_floats_MAXFREELIST == 100 entries); on a miss it allocates
sizeof(PyFloatObject) bytes with PyObject_Malloc and initializes the
type. The freelist is LIFO and linked through the first word of the object
(the same _Py_FREELIST_POP/_Py_FREELIST_FREE machinery used by ints).
Dealloc and the freelist
// Objects/floatobject.c
static void
float_dealloc(PyObject *op)
{
assert(PyFloat_Check(op));
if (PyFloat_CheckExact(op))
_PyFloat_ExactDealloc(op);
else
Py_TYPE(op)->tp_free(op);
}
void
_PyFloat_ExactDealloc(PyObject *obj)
{
assert(PyFloat_CheckExact(obj));
_Py_FREELIST_FREE(floats, obj, PyObject_Free);
}Exact floats go back to the freelist; subclasses fall through to
tp_free (PyObject_Free). _PyFloat_DebugMallocStats reports the freelist
depth via _Py_FREELIST_SIZE(floats).
Operations
Every binary operation follows the same shape: convert both operands to
double, do the C operation, box the result.
// Objects/floatobject.c
#define CONVERT_TO_DOUBLE(obj, dbl) \
if (PyFloat_Check(obj)) \
dbl = PyFloat_AS_DOUBLE(obj); \
else if (_Py_convert_int_to_double(&(obj), &(dbl)) < 0) \
return obj;_Py_convert_int_to_double handles the int case by calling
PyLong_AsDouble (and returns Py_NotImplemented for anything else, so
the generic number protocol can try the reflected operation):
// Objects/floatobject.c
int
_Py_convert_int_to_double(PyObject **v, double *dbl)
{
PyObject *obj = *v;
if (PyLong_Check(obj)) {
*dbl = PyLong_AsDouble(obj);
if (*dbl == -1.0 && PyErr_Occurred()) {
*v = NULL;
return -1;
}
}
else {
*v = Py_NewRef(Py_NotImplemented);
return -1;
}
return 0;
}A typical op, addition:
// Objects/floatobject.c
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a, b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
a = a + b;
return PyFloat_FromDouble(a);
}float_div adds an explicit zero-denominator check raising
ZeroDivisionError; float_rem/_float_div_mod use fmod and fix up the
sign of the remainder to match the denominator. float_pow special-cases
nan, inf, zero bases, negative bases raised to fractional powers (which
delegates to complex), and the libm-bug-prone (-1)**large_int case
before finally calling pow().
Comparison with ints
Comparing a float to an int cannot simply convert either side, because large
ints overflow double and small fractional parts matter. float_richcompare
handles the cases:
- If the float is non-finite, any finite int compares as smaller in magnitude (and NaN yields the usual NaN comparison rules).
- If the int has more than
DBL_MAX_EXPbits it is larger than any finite double. - If the int fits in 48 bits it is converted with
PyLong_AsDouble. - Otherwise the number of bits before the radix point is compared; if they
differ the result follows from that. If they match, the float’s integer
part is converted to an int (
PyLong_FromDouble) and the two ints are compared withPyObject_RichCompareBool, with special handling for non-integer floats (which can never equal an int).
Float-to-float comparison is just the platform C ==, <=, etc., applied
to ob_fval, so NaN behaves as C NaN.
Hashing
// Objects/floatobject.c
static Py_hash_t
float_hash(PyObject *op)
{
PyFloatObject *v = _PyFloat_CAST(op);
return _Py_HashDouble(op, v->ob_fval);
}_Py_HashDouble (in Python/pyhash.c) maps the double to a hash in
[0, PyHASH_MODULUS):
// Python/pyhash.c
Py_hash_t
_Py_HashDouble(PyObject *inst, double v)
{
int e, sign;
double m;
Py_uhash_t x, y;
if (!isfinite(v)) {
if (isinf(v))
return v > 0 ? PyHASH_INF : -PyHASH_INF;
else
return PyObject_GenericHash(inst); // NaN -> id-based
}
m = frexp(v, &e);
sign = 1;
if (m < 0) { sign = -1; m = -m; }
x = 0;
while (m) {
x = ((x << 28) & PyHASH_MODULUS) | x >> (PyHASH_BITS - 28);
m *= 268435456.0; // 2**28
e -= 28;
y = (Py_uhash_t)m; // pull out integer part
m -= y;
x += y;
if (x >= PyHASH_MODULUS)
x -= PyHASH_MODULUS;
}
e = e >= 0 ? e % PyHASH_BITS : PyHASH_BITS-1-((-1-e) % PyHASH_BITS);
x = ((x << e) & PyHASH_MODULUS) | x >> (PyHASH_BITS - e);
x = x * sign;
if (x == (Py_uhash_t)-1)
x = (Py_uhash_t)-2;
return (Py_hash_t)x;
}Infinities hash to fixed sentinels PyHASH_INF/-PyHASH_INF; NaNs (not
equal to themselves) get a generic object hash based on identity. The
rotation tricks rely on 2**PyHASH_BITS ≡ 1 (mod PyHASH_MODULUS).
repr and str
tp_str is NULL, so str() falls back to tp_repr:
// Objects/floatobject.c
static PyObject *
float_repr(PyObject *op)
{
PyFloatObject *v = _PyFloat_CAST(op);
PyObject *result;
char *buf;
buf = PyOS_double_to_string(PyFloat_AS_DOUBLE(v),
'r', 0,
Py_DTSF_ADD_DOT_0,
NULL);
if (!buf)
return PyErr_NoMemory();
result = _PyUnicode_FromASCII(buf, strlen(buf));
PyMem_Free(buf);
return result;
}PyOS_double_to_string with 'r' uses 17 significant digits (the minimum
that round-trips an IEEE double) and Py_DTSF_ADD_DOT_0 forces a trailing
.0 so 1.0 prints as "1.0" rather than "1". The conversion itself
goes through the correctly-rounded dtoa routines in Python/dtoa.c
(_Py_dg_dtoa/_Py_dg_strtod) when _PY_SHORT_FLOAT_REPR is enabled.
float.__format__ and format() use the same machinery with the requested
precision and type code.
Conversion to and from Python
float(x)for an exactstr/bytes/bytearray/buffer callsPyFloat_FromString, which strips whitespace, callsPyOS_string_to_double(and_Py_parse_inf_or_nanforinf/nan), thenPyFloat_FromDouble.float(x)for a number callsPyNumber_Float, which for an int goes throughPyFloat_AsDouble->PyLong_AsDouble.PyFloat_AsDouble(op)returnsob_fvaldirectly for floats; otherwise it invokesnb_float, or falls back tonb_index+PyLong_AsDouble.PyLong_AsDouble(used by_Py_convert_int_to_double) returns the double value of an int, setting overflow errors when out of range.float.as_integer_ratio()usesfrexpto split the value into mantissa and exponent and builds a numerator/denominator pair ofints.
nan / inf handling
_Py_parse_inf_or_nan(inPython/pystrtod.c) recognizesinf,infinity, andnanwith an optional leading sign, returningINFINITY/Py_NAN.isfinite/isnan/isinf(from<math.h>) gate special cases infloat_pow,float_richcompare,_Py_HashDouble,float_as_integer_ratio, andfloat_is_integer.float_divraisesZeroDivisionErroronly for an exactly zero denominator;0.0/0.0producesnanand1.0/0.0producesinfvia IEEE rules otherwise.float.as_integer_ratio()raisesOverflowErrorfor infinities andValueErrorfor NaNs.
Memory layout
+---------------------------+ <- PyObject_Malloc result
| ob_refcnt |
+---------------------------+
| *ob_type (&PyFloat_Type) |
+---------------------------+
| ob_fval (double, 8 B) |
+---------------------------+Total size is sizeof(PyFloatObject) (24 bytes on a 64-bit build: two
pointer-sized words plus the 8-byte double). The freelist recycles the
entire object; there is no trailing array to size. Subclasses are allocated
through tp_alloc (PyObject_Malloc of tp_basicsize) and initialized
from a temporary regular float in float_subtype_new.
Key takeaways
floatis aPyObjectwhose only payload isdouble ob_fval.PyFloat_FromDoublepops from a 100-deep LIFO freelist before allocating.- Arithmetic converts both sides to
doubleviaCONVERT_TO_DOUBLE, then boxes the result; ints are converted withPyLong_AsDouble. repr/strusePyOS_double_to_string(..., 'r', ..., Py_DTSF_ADD_DOT_0)(17 significant digits, always with.0).- Hashing folds the mantissa/exponent into
PyHASH_MODULUS; infinities and NaNs are special-cased. - nan/inf propagate per IEEE rules, with explicit guards in pow, hashing,
comparison, and
as_integer_ratio.
Further reading
Objects/floatobject.c– all of the above.Include/cpython/floatobject.h–PyFloatObjectandPyFloat_AS_DOUBLE.Include/internal/pycore_floatobject.h– freelist dealloc,_Py_HashDoubleentry point,_Py_parse_inf_or_nan.Python/pyhash.c–_Py_HashDouble.Python/pystrtod.c–PyOS_double_to_string,_Py_parse_inf_or_nan.Python/dtoa.c– correctly-rounded double<->string conversion.