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:

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

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

c
// 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_basicsize is 0 because instances are never created through tp_alloc; the only instance is the static _Py_EllipsisObject.
  • tp_flags is just Py_TPFLAGS_DEFAULT – no Py_TPFLAGS_HAVE_GC, so the singleton is not tracked by the garbage collector.
  • tp_getattro is PyObject_GenericGetAttr, so attribute lookup works normally (e.g. Ellipsis.__reduce__).
  • tp_hash is 0, meaning the default identity-based hash is used; because there is only one instance this is effectively a constant.
  • tp_richcompare is 0. Equality falls back to identity, so Ellipsis == Ellipsis is True and Ellipsis == 42 is False.

The type is exported to Python as types.EllipsisType via Modules/_typesmodule.c:

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:

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

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

c
// 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) and str(Ellipsis) both yield "Ellipsis" (tp_str is 0, so str() falls back to repr()).
  • __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__:
c
// 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:

c
// 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_Ellipsis is simply the address of the static struct.
  • In the limited API (3.13+), Py_Ellipsis is fetched from the interpreter’s constants table via Py_GetConstantBorrowed, so extension modules never reference the private _Py_EllipsisObject symbol 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:

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

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 ... is True, and id(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:

c
// 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.c and Objects/genericaliasobject.c compare pointers against Py_Ellipsis to detect ... in slicing/subscripting contexts.
  • Objects/typevarobject.c checks p == Py_Ellipsis when unpacking type variables (the ... “unbounded” marker).
  • Objects/codeobject.c treats Py_Ellipsis (alongside Py_None) as a singleton when serializing code constants.

Memory layout

_Py_EllipsisObject is just an object header – no extra fields:

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

  • Ellipsis is a statically allocated, immortal PyObject named _Py_EllipsisObject, defined in Objects/sliceobject.c.
  • Its type PyEllipsis_Type has tp_basicsize == 0 and no GC flags; the only instance is the static singleton.
  • ellipsis_new rejects arguments and returns Py_Ellipsis; ellipsis_dealloc re-immortalizes as a guard.
  • Repr and __reduce__ both produce "Ellipsis".
  • Py_Ellipsis resolves to &_Py_EllipsisObject in the internal API, or to Py_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_Ellipsis macro and PyEllipsis_Type.
  • Include/internal/pycore_object.h – _PyObject_HEAD_INIT and immortal refcount constants.
  • Objects/object.c – constants[], _Py_GetConstant_Init, Py_GetConstantBorrowed, _Py_SetImmortal.
  • Modules/_typesmodule.c – export of EllipsisType.
  • Python/compile.c – identity-based handling of Ellipsis in const cache.
  • Lib/test/test_types.py – EllipsisType coverage.

PythonCpythonInternalsEllipsis