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:
// 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:
// 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:
// 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:
// 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):
// 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
// 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 !=:
// 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
// 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
// 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
+----------------------------------+ <- 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
complexis aPyObjectwhose payload isPy_complex: two contiguousdoubles (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_complexroutines (with C11 Annex G infinity/NaN recovery) and aCOMPLEX_BINOPmacro that mixes real and complex operands without an intermediate complex. - Only
==and!=are defined; ordering raisesTypeError. - Hashing combines the two component hashes as
hashreal + 1000003*hashimag, ensuringhash(x+0j) == hash(x). repruses 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_complexandPyComplexObject.Include/internal/pycore_freelist_state.h–Py_complexes_MAXFREELIST.Python/pyhash.c–_Py_HashDouble.Doc/c-api/complex.rst– public C API.