CPython's Ellipsis Implementation in C
Ellipsis as an immortal singleton: _Py_EllipsisObject, PyEllipsis_Type, ellipsis_new/dealloc, the Py_Ellipsis macro, the constants table, and immortal refcounts traced through the CPython 3.15 source.
Python’s Ellipsis is the object spelled .... It is a singleton: there is
exactly one instance, and type(Ellipsis) is types.EllipsisType. In CPython
it lives in Objects/sliceobject.c alongside slice, and is implemented as a
statically allocated, immortal PyObject whose type is PyEllipsis_Type. This
article traces that implementation through the 3.15 source.
The singleton: _Py_EllipsisObject
The entire object is a single global struct with no payload beyond the object header:
// Objects/sliceobject.c
PyObject _Py_EllipsisObject = _PyObject_HEAD_INIT(&PyEllipsis_Type);There is no tp_alloc, no freelist, and no _PyObject_GC_New. The object
exists for the lifetime of the interpreter because it is a file-scope (static
storage) variable. Its initializer uses _PyObject_HEAD_INIT, which, on 64-bit
builds, sets an immortal reference count and the static flag bits:
// Include/internal/pycore_object.h
#if SIZEOF_VOID_P > 4
#define _PyObject_HEAD_INIT(type) \
{ \
.ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT, \
.ob_flags = _Py_STATIC_FLAG_BITS, \
.ob_type = (type) \
}
#endif_Py_IMMORTAL_INITIAL_REFCNT is (3ULL << 30), a value chosen to be far
above any realistic reference count so that increment/decrement operations never
reach zero. The object is therefore immortal from the moment the module is
loaded – no runtime call is needed to “make” it immortal.
The type: PyEllipsis_Type
PyEllipsis_Type is a plain (non-GC) type with a zero basic size, since no
instances are ever allocated:
// Objects/sliceobject.c
PyTypeObject PyEllipsis_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"ellipsis", /* tp_name */
0, /* tp_basicsize */
0, /* tp_itemsize */
ellipsis_dealloc, /* tp_dealloc */
...
ellipsis_repr, /* tp_repr */
...
PyObject_GenericGetAttr, /* tp_getattro */
...
Py_TPFLAGS_DEFAULT, /* tp_flags */
ellipsis_doc, /* tp_doc */
...
ellipsis_methods, /* tp_methods */
...
ellipsis_new, /* tp_new */
};Key points:
tp_basicsizeis0because instances are never created throughtp_alloc; the only instance is the static_Py_EllipsisObject.tp_flagsis justPy_TPFLAGS_DEFAULT– noPy_TPFLAGS_HAVE_GC, so the singleton is not tracked by the garbage collector.tp_getattroisPyObject_GenericGetAttr, so attribute lookup works normally (e.g.Ellipsis.__reduce__).tp_hashis0, meaning the default identity-based hash is used; because there is only one instance this is effectively a constant.tp_richcompareis0. Equality falls back to identity, soEllipsis == EllipsisisTrueandEllipsis == 42isFalse.
The type is exported to Python as types.EllipsisType via
Modules/_typesmodule.c:
// Modules/_typesmodule.c
EXPORT_STATIC_TYPE("EllipsisType", PyEllipsis_Type);Construction: ellipsis_new
Calling EllipsisType() (or type(Ellipsis)()) must return the singleton
rather than allocate a new object. ellipsis_new enforces that no arguments are
passed and returns Py_Ellipsis:
// Objects/sliceobject.c
static PyObject *
ellipsis_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
if (PyTuple_GET_SIZE(args) || (kwargs && PyDict_GET_SIZE(kwargs))) {
PyErr_SetString(PyExc_TypeError, "EllipsisType takes no arguments");
return NULL;
}
return Py_Ellipsis;
}This is why type(Ellipsis)() produces the same singleton, as documented in
the standard library docs. ellipsis_new does not touch reference counts; it
returns a borrowed reference in the CPython convention used by tp_new.
Dealloc guard: ellipsis_dealloc
Because the object is immortal, tp_dealloc should never run. It exists only
as a safety net in case a bug drives the reference count to zero. Instead of
freeing memory (which would be catastrophic for a static object), it
re-immortalizes the object:
// Objects/sliceobject.c
static void
ellipsis_dealloc(PyObject *ellipsis)
{
/* This should never get called, but we also don't want to SEGV if
* we accidentally decref Ellipsis out of existence. Instead,
* since Ellipsis is an immortal object, re-set the reference count.
*/
_Py_SetImmortal(ellipsis);
}_Py_SetImmortal untracks the object from the GC (if it were tracked) and
resets ob_refcnt/ob_flags to the immortal values. The same pattern is used
by none_dealloc and notimplemented_dealloc for None and NotImplemented.
Repr and pickling
// Objects/sliceobject.c
static PyObject *
ellipsis_repr(PyObject *op)
{
return PyUnicode_FromString("Ellipsis");
}
static PyObject *
ellipsis_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
{
return PyUnicode_FromString("Ellipsis");
}repr(Ellipsis)andstr(Ellipsis)both yield"Ellipsis"(tp_stris0, sostr()falls back torepr()).__reduce__returns the string"Ellipsis", which pickles/unpickles by name lookup, so a round-trip produces the same singleton.- The method table exposes only
__reduce__:
// Objects/sliceobject.c
static PyMethodDef ellipsis_methods[] = {
{"__reduce__", ellipsis_reduce, METH_NOARGS, NULL},
{NULL, NULL}
};The public name: Py_Ellipsis
How C code obtains the singleton depends on the API mode:
// Include/sliceobject.h
PyAPI_DATA(PyObject) _Py_EllipsisObject; /* Don't use this directly */
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_Ellipsis Py_GetConstantBorrowed(Py_CONSTANT_ELLIPSIS)
#else
# define Py_Ellipsis (&_Py_EllipsisObject)
#endif- In the unlimited (internal) API,
Py_Ellipsisis simply the address of the static struct. - In the limited API (3.13+),
Py_Ellipsisis fetched from the interpreter’s constants table viaPy_GetConstantBorrowed, so extension modules never reference the private_Py_EllipsisObjectsymbol directly. This is what makes the symbol part of the stable ABI story.
Py_GetConstantBorrowed returns the same pointer every time; all constants are
immortal by construction:
// Objects/object.c
PyObject*
Py_GetConstantBorrowed(unsigned int constant_id)
{
// All constants are immortal
return Py_GetConstant(constant_id);
}The constants table
The limited-API path resolves to a slot in a static array in Objects/object.c:
// Objects/object.c
static PyObject* constants[] = {
&_Py_NoneStruct, // Py_CONSTANT_NONE
(PyObject*)(&_Py_FalseStruct), // Py_CONSTANT_FALSE
(PyObject*)(&_Py_TrueStruct), // Py_CONSTANT_TRUE
&_Py_EllipsisObject, // Py_CONSTANT_ELLIPSIS
&_Py_NotImplementedStruct, // Py_CONSTANT_NOT_IMPLEMENTED
NULL, // Py_CONSTANT_ZERO
...
};
void
_Py_GetConstant_Init(void)
{
constants[Py_CONSTANT_ZERO] = _PyLong_GetZero();
constants[Py_CONSTANT_EMPTY_STR] = PyUnicode_New(0, 0);
...
#ifndef NDEBUG
for (size_t i=0; i < Py_ARRAY_LENGTH(constants); i++) {
assert(constants[i] != NULL);
assert(_Py_IsImmortal(constants[i]));
}
#endif
}So _Py_EllipsisObject is both a standalone global and the value stored at
Py_CONSTANT_ELLIPSIS. In debug builds the interpreter asserts that every
constant, including Ellipsis, is immortal before startup completes.
Immortality in practice
Because _Py_EllipsisObject is statically allocated with an immortal refcount:
Py_INCREF(Py_Ellipsis)/Py_DECREF(Py_Ellipsis)are effectively no-ops (the refcount never reaches zero).- It is not GC-tracked, so it never participates in collection cycles.
PyUnstable_Object_EnableDeferredRefcount/ deferred refcounting does not apply to it (it is not a GC object).- Its identity is stable for the whole process:
Ellipsis is ...isTrue, andid(Ellipsis)never changes.
The same immortality is what lets the compiler treat Ellipsis specially in
constant folding. In Python/compile.c, None and Ellipsis are detected by
identity and used directly as constant keys without going through
_PyCode_ConstantKey:
// Python/compile.c
// None and Ellipsis are immortal objects, and key is the singleton.
// No need to merge object and key.
if (o == Py_None || o == Py_Ellipsis) {
return o;
}Uses of Py_Ellipsis in the core
A few places in the C core rely on the singleton identity:
Objects/memoryview.candObjects/genericaliasobject.ccompare pointers againstPy_Ellipsisto detect...in slicing/subscripting contexts.Objects/typevarobject.cchecksp == Py_Ellipsiswhen unpacking type variables (the...“unbounded” marker).Objects/codeobject.ctreatsPy_Ellipsis(alongsidePy_None) as a singleton when serializing code constants.
Memory layout
_Py_EllipsisObject is just an object header – no extra fields:
_Py_EllipsisObject (static storage, 16 bytes on 64-bit)
+----------------------------+
| ob_refcnt (immortal) | <- _Py_IMMORTAL_INITIAL_REFCNT
+----------------------------+
| ob_flags (static bits) |
+----------------------------+
| ob_type -> &PyEllipsis_Type |
+----------------------------+The ABI data file records its size as 16 bytes (Doc/data/python3.15.abi),
matching sizeof(PyObject) on 64-bit. There is no tp_traverse, no
tp_clear, and no GC tracking, because there are no owned references to visit.
Key takeaways
Ellipsisis a statically allocated, immortalPyObjectnamed_Py_EllipsisObject, defined inObjects/sliceobject.c.- Its type
PyEllipsis_Typehastp_basicsize == 0and no GC flags; the only instance is the static singleton. ellipsis_newrejects arguments and returnsPy_Ellipsis;ellipsis_deallocre-immortalizes as a guard.- Repr and
__reduce__both produce"Ellipsis". Py_Ellipsisresolves to&_Py_EllipsisObjectin the internal API, or toPy_GetConstantBorrowed(Py_CONSTANT_ELLIPSIS)in the limited API.- The object is immortal from static initialization, is not GC-tracked, and is used by identity throughout the interpreter and compiler.
Further reading
Objects/sliceobject.c–PyEllipsis_Type,_Py_EllipsisObject,ellipsis_new,ellipsis_dealloc,ellipsis_repr,ellipsis_reduce.Include/sliceobject.h–Py_Ellipsismacro andPyEllipsis_Type.Include/internal/pycore_object.h–_PyObject_HEAD_INITand immortal refcount constants.Objects/object.c–constants[],_Py_GetConstant_Init,Py_GetConstantBorrowed,_Py_SetImmortal.Modules/_typesmodule.c– export ofEllipsisType.Python/compile.c– identity-based handling ofEllipsisin const cache.Lib/test/test_types.py–EllipsisTypecoverage.