How CPython Implements complex in C

The PyComplexObject struct, Py_complex with real/imag doubles, mixed-mode arithmetic, hashing, repr, allocation, and memory layout traced through the CPython 3.15 source.

Python’s complex is a fixed-size value type: two C doubles, one for the real part and one for the imaginary part. There is no arbitrary precision and no digit array, unlike int. In CPython it is implemented by PyComplexObject in Objects/complexobject.c, and this article traces the representation and the hot paths through the 3.15 source.

The object struct

Every Python object starts with PyObject_HEAD (ob_refcnt and ob_type). For complex numbers the payload is a single Py_complex, which is just two doubles:

c
// Include/cpython/complexobject.h
typedef struct {
    double real;
    double imag;
} Py_complex;

typedef struct {
    PyObject_HEAD
    Py_complex cval;
} PyComplexObject;

Py_complex is also exposed as a plain C struct for the C API (PyComplex_FromCComplex, PyComplex_AsCComplex), so extensions can pass complex values around without touching a PyObject. The real and imag members are published as read-only attributes via complex_members using offsetof(PyComplexObject, cval.real) and ...cval.imag.

There is no cache

Unlike int, complex has no small-value cache and no immortal singletons. Every complex() call (other than the exact-type single-argument fast path in actual_complex_new) produces a fresh object. What it does have is a freelist.

Allocation and the freelist

PyComplex_FromCComplex is the single hot allocation entry point used by almost all operations:

c
// Objects/complexobject.c
PyObject *
PyComplex_FromCComplex(Py_complex cval)
{
    PyComplexObject *op = _Py_FREELIST_POP(PyComplexObject, complexes);

    if (op == NULL) {
        /* Inline PyObject_New */
        op = PyObject_Malloc(sizeof(PyComplexObject));
        if (op == NULL) {
            return PyErr_NoMemory();
        }
        _PyObject_Init((PyObject*)op, &PyComplex_Type);
    }
    op->cval = cval;
    return (PyObject *) op;
}

The freelist is LIFO and holds up to Py_complexes_MAXFREELIST (100) objects, linked through the first word of the object. Subtype construction goes through complex_subtype_from_c_complex, which uses tp_alloc (PyType_GenericAlloc) instead, so it always allocates fresh memory and initializes cval afterwards.

Deallocation mirrors this:

c
// Objects/complexobject.c
static void
complex_dealloc(PyObject *op)
{
    assert(PyComplex_Check(op));
    if (PyComplex_CheckExact(op)) {
        _Py_FREELIST_FREE(complexes, op, PyObject_Free);
    }
    else {
        Py_TYPE(op)->tp_free(op);
    }
}

Only exact complex instances are recycled; subclasses are freed directly. complex_pos also avoids allocation when possible by returning a new reference to an exact complex.

Elementary operations on Py_complex

The arithmetic lives in a layer of pure functions on Py_complex values, independent of PyObject:

c
// Objects/complexobject.c
Py_complex
_Py_c_sum(Py_complex a, Py_complex b)
{
    Py_complex r;
    r.real = a.real + b.real;
    r.imag = a.imag + b.imag;
    return r;
}

Py_complex
_Py_c_prod(Py_complex z, Py_complex w)
{
    double a = z.real, b = z.imag, c = w.real, d = w.imag;
    double ac = a*c, bd = b*d, ad = a*d, bc = b*c;
    Py_complex r = {ac - bd, ad + bc};
    /* Recover infinities that computed as nan+nanj ... */
    ...
    return r;
}

_Py_c_prod is the interesting one: the naive formula (a+bi)(c+di) = (ac-bd)+(ad+bc)i can produce nan+nanj for infinities and overflows, so the implementation follows C11 Annex G and re-derives the result with boxed infinities when that happens. _Py_c_quot similarly uses Smith’s algorithm (divide numerator and denominator by the larger of |b.real|, |b.imag|) to avoid spurious overflow, with infinity/zero recovery afterwards. _Py_c_pow uses polar form (hypot, atan2, pow, exp, cos, sin) and _Py_c_abs is hypot with special handling for infinities and NaNs.

There are also mixed real/complex variants (_Py_cr_*, _Py_rc_*) that avoid constructing an intermediate complex when one operand is a plain double.

Mixed-mode binary ops

The PyObject-level operators are generated by the COMPLEX_BINOP macro. It handles four cases: both operands complex, complex + real, real + complex, and neither complex (returns NotImplemented):

c
// Objects/complexobject.c
#define COMPLEX_BINOP(NAME, FUNC)                           \
    static PyObject *                                       \
    complex_##NAME(PyObject *v, PyObject *w)                \
    {                                                       \
        Py_complex a;                                       \
        errno = 0;                                          \
        if (PyComplex_Check(w)) {                           \
            Py_complex b = ((PyComplexObject *)w)->cval;    \
            if (PyComplex_Check(v)) {                       \
                a = ((PyComplexObject *)v)->cval;           \
                a = _Py_c_##FUNC(a, b);                     \
            }                                               \
            else if (real_to_double(&v, &a.real) < 0) {     \
                return v;                                   \
            }                                               \
            else {                                          \
                a = _Py_rc_##FUNC(a.real, b);               \
            }                                               \
        }                                                   \
        else if (!PyComplex_Check(v)) {                     \
            Py_RETURN_NOTIMPLEMENTED;                       \
        }                                                   \
        else {                                              \
            a = ((PyComplexObject *)v)->cval;               \
            double b;                                       \
            if (real_to_double(&w, &b) < 0) {               \
                return w;                                   \
            }                                               \
            a = _Py_cr_##FUNC(a, b);                        \
        }                                                   \
        if (errno == EDOM) {                                \
            PyErr_SetString(PyExc_ZeroDivisionError,        \
                            "division by zero");            \
            return NULL;                                    \
        }                                                   \
        return PyComplex_FromCComplex(a);                   \
    }

real_to_double converts an int/float operand to a double, including the int-to-double conversion path via _Py_convert_int_to_double. complex_pow additionally detects a small integer exponent (b.imag == 0, integral, |b.real| <= 100) and uses the faster, more accurate c_powi binary exponentiation instead of the polar-form _Py_c_pow.

Truthiness, negation, absolute value

c
// Objects/complexobject.c
static int
complex_bool(PyObject *op)
{
    PyComplexObject *v = _PyComplexObject_CAST(op);
    return v->cval.real != 0.0 || v->cval.imag != 0.0;
}

static PyObject *
complex_abs(PyObject *op)
{
    PyComplexObject *v = _PyComplexObject_CAST(op);
    double result;
    errno = 0;
    result = _Py_c_abs(v->cval);
    if (errno == ERANGE) {
        PyErr_SetString(PyExc_OverflowError, "absolute value too large");
        return NULL;
    }
    return PyFloat_FromDouble(result);
}

complex_abs returns a float, not a complex. Negation and conjugation build a new Py_complex and call PyComplex_FromCComplex.

Comparison

Complex numbers support only == and !=:

c
// Objects/complexobject.c
static PyObject *
complex_richcompare(PyObject *v, PyObject *w, int op)
{
    ...
    if (op != Py_EQ && op != Py_NE) {
        goto Unimplemented;   /* ordering raises TypeError via default */
    }
    ...
    if (PyLong_Check(w)) {
        if (i.imag == 0.0) {
            /* compare as floats to reuse int/float equality rules */
            PyObject *j = PyFloat_FromDouble(i.real);
            ...
        } else {
            equal = 0;
        }
    }
    else if (PyFloat_Check(w)) {
        equal = (i.real == PyFloat_AsDouble(w) && i.imag == 0.0);
    }
    else if (PyComplex_Check(w)) {
        Py_complex j;
        TO_COMPLEX(w, j);
        equal = (i.real == j.real && i.imag == j.imag);
    }
    ...
}

When the imaginary part is zero and the other operand is an int, it delegates to float/int comparison so that complex(2, 0) == 2 is true and hashes match.

Hashing

c
// Objects/complexobject.c
static Py_hash_t
complex_hash(PyObject *op)
{
    Py_uhash_t hashreal, hashimag, combined;
    PyComplexObject *v = _PyComplexObject_CAST(op);
    hashreal = (Py_uhash_t)_Py_HashDouble(op, v->cval.real);
    if (hashreal == (Py_uhash_t)-1) return -1;
    hashimag = (Py_uhash_t)_Py_HashDouble(op, v->cval.imag);
    if (hashimag == (Py_uhash_t)-1) return -1;
    /* if the imaginary part is 0, hashimag is 0 now, so this returns
       hashreal unchanged -- required so that hash(x+0j) == hash(x). */
    combined = hashreal + PyHASH_IMAG * hashimag;
    if (combined == (Py_uhash_t)-1) combined = (Py_uhash_t)-2;
    return (Py_hash_t)combined;
}

_Py_HashDouble reduces each double modulo 2**PyHASH_BITS - 1 (a Mersenne prime) using bit rotations. PyHASH_IMAG is PyHASH_MULTIPLIER (1000003). The combined value is hashreal + 1000003 * hashimag, which keeps hash(complex(x, 0)) == hash(x) because hashimag becomes 0 for a +0.0 imaginary part (and NaNs fall back to PyObject_GenericHash).

Repr

c
// Objects/complexobject.c
static PyObject *
complex_repr(PyObject *op)
{
    ...
    if (v->cval.real == 0. && copysign(1.0, v->cval.real)==1.0) {
        /* Real part is +0: just output the imaginary part. */
        re = "";
        im = PyOS_double_to_string(v->cval.imag, 'r', 0, 0, NULL);
    } else {
        pre = PyOS_double_to_string(v->cval.real, 'r', 0, 0, NULL);
        re = pre;
        im = PyOS_double_to_string(v->cval.imag, 'r', 0, Py_DTSF_SIGN, NULL);
        lead = "("; tail = ")";
    }
    result = PyUnicode_FromFormat("%s%s%sj%s", lead, re, im, tail);
    ...
}

A pure-imaginary or zero-real number prints without parentheses (3j, 1j); anything with a nonzero real part is wrapped in parentheses and the imaginary part is always emitted with an explicit sign ((3+4j), (1-2j)). The PyOS_double_to_string 'r' mode gives the shortest round-tripping representation.

Construction paths

complex() has two entry points. actual_complex_new handles zero or one positional argument: an exact complex is returned as-is, a string is parsed by complex_from_string_inner, a complex subclass is copied by value, and anything with nb_float/nb_index is treated as a real number. Otherwise complex_new_impl handles complex(real, imag), including the deprecated case where either argument is itself complex, by combining cr.imag/ci.imag into the result.

Memory layout

text
+----------------------------------+  <- PyObject_Malloc result
| ob_refcnt                        |   8 bytes
+----------------------------------+
| *ob_type  (&PyComplex_Type)      |   8 bytes
+----------------------------------+
| cval.real                        |   8 bytes
+----------------------------------+
| cval.imag                        |   8 bytes
+----------------------------------+

Total sizeof(PyComplexObject) is 32 bytes. There is no separate allocation for the payload (it is embedded), no GC tracking (complex holds no references to other objects), and no weakref list. Freed exact instances are pushed onto the 100-deep complexes freelist and reused.

Key takeaways

  • complex is a PyObject whose payload is Py_complex: two contiguous doubles (real, imag), for a 32-byte object.
  • There is no cache or small-int-style singleton; allocation goes through a 100-deep freelist for exact instances.
  • Arithmetic is implemented twice: pure Py_complex routines (with C11 Annex G infinity/NaN recovery) and a COMPLEX_BINOP macro that mixes real and complex operands without an intermediate complex.
  • Only == and != are defined; ordering raises TypeError.
  • Hashing combines the two component hashes as hashreal + 1000003*hashimag, ensuring hash(x+0j) == hash(x).
  • repr uses shortest round-tripping doubles, with parentheses only when the real part is nonzero.

Further reading

  • Objects/complexobject.c – all of the above.
  • Include/cpython/complexobject.h – Py_complex and PyComplexObject.
  • Include/internal/pycore_freelist_state.h – Py_complexes_MAXFREELIST.
  • Python/pyhash.c – _Py_HashDouble.
  • Doc/c-api/complex.rst – public C API.

PythonCpythonInternalsComplex