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

c
// 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:

c
// 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

c
// 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.

c
// 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):

c
// 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:

c
// 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_EXP bits 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 with PyObject_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

c
// 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):

c
// 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:

c
// 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 exact str/bytes/bytearray/buffer calls PyFloat_FromString, which strips whitespace, calls PyOS_string_to_double (and _Py_parse_inf_or_nan for inf/nan), then PyFloat_FromDouble.
  • float(x) for a number calls PyNumber_Float, which for an int goes through PyFloat_AsDouble -> PyLong_AsDouble.
  • PyFloat_AsDouble(op) returns ob_fval directly for floats; otherwise it invokes nb_float, or falls back to nb_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() uses frexp to split the value into mantissa and exponent and builds a numerator/denominator pair of ints.

nan / inf handling

  • _Py_parse_inf_or_nan (in Python/pystrtod.c) recognizes inf, infinity, and nan with an optional leading sign, returning INFINITY/Py_NAN.
  • isfinite/isnan/isinf (from <math.h>) gate special cases in float_pow, float_richcompare, _Py_HashDouble, float_as_integer_ratio, and float_is_integer.
  • float_div raises ZeroDivisionError only for an exactly zero denominator; 0.0/0.0 produces nan and 1.0/0.0 produces inf via IEEE rules otherwise.
  • float.as_integer_ratio() raises OverflowError for infinities and ValueError for NaNs.

Memory layout

text
+---------------------------+  <- 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

  • float is a PyObject whose only payload is double ob_fval.
  • PyFloat_FromDouble pops from a 100-deep LIFO freelist before allocating.
  • Arithmetic converts both sides to double via CONVERT_TO_DOUBLE, then boxes the result; ints are converted with PyLong_AsDouble.
  • repr/str use PyOS_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 – PyFloatObject and PyFloat_AS_DOUBLE.
  • Include/internal/pycore_floatobject.h – freelist dealloc, _Py_HashDouble entry 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.

PythonCpythonInternalsFloat