CPython's NotImplemented Implementation in C
The NotImplemented singleton, its NotImplementedType, immortal static storage, the boolean-context guard, and how binary operators and rich comparisons consume it, traced through the CPython 3.15 source.
Python’s NotImplemented is not an exception and not a function result that
means “I gave up.” It is a real singleton object of type NotImplementedType,
returned from operator implementations to tell the interpreter: “this operand
combination is not supported by me – try the other operand, or fall back.”
In CPython that is implemented in Objects/object.c as a single static
PyObject struct, _Py_NotImplementedStruct, whose type is
_PyNotImplemented_Type. This article traces that implementation through the
3.15 source.
The type object
NotImplementedType is a plain, non-subclassable-in-practice type whose slot
table is almost entirely zero. The few slots that are set define its identity
and its role as a sentinel:
// Objects/object.c
PyTypeObject _PyNotImplemented_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"NotImplementedType",
0, /* tp_basicsize: sizeof(PyObject) */
0, /* tp_itemsize */
notimplemented_dealloc, /* tp_dealloc */ /*never called*/
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
NotImplemented_repr, /* tp_repr */
¬implemented_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
...
Py_TPFLAGS_DEFAULT, /* tp_flags */
notimplemented_doc, /* tp_doc */
...
0, /* tp_base */ /* filled in by PyType_Ready -> object */
...
notimplemented_methods, /* tp_methods */
...
0, /* tp_init */
0, /* tp_alloc */
notimplemented_new, /* tp_new */
};tp_basicsize is 0, which PyType_Ready turns into sizeof(PyObject): the
singleton carries no payload at all, it is just a head. tp_base is 0, so
PyType_Ready makes object the base, which is why NotImplemented inherits
the default identity equality and identity hash from object.
The type is exported to Python as types.NotImplementedType:
// Modules/_typesmodule.c
EXPORT_STATIC_TYPE("NotImplementedType", _PyNotImplemented_Type);The singleton
There is exactly one instance, statically allocated at compile time:
// Objects/object.c
PyObject _Py_NotImplementedStruct = _PyObject_HEAD_INIT(&_PyNotImplemented_Type);_PyObject_HEAD_INIT initializes the refcount to an immortal value, sets the
static-allocation flag, and points ob_type at _PyNotImplemented_Type. No
runtime allocation ever happens.
The public macro resolves to that struct (or, in the limited API 3.13+, to a borrowed reference from the interpreter’s constant table):
// Include/object.h
PyAPI_DATA(PyObject) _Py_NotImplementedStruct; /* Don't use this directly */
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030D0000
# define Py_NotImplemented Py_GetConstantBorrowed(Py_CONSTANT_NOT_IMPLEMENTED)
#else
# define Py_NotImplemented (&_Py_NotImplementedStruct)
#endifPy_RETURN_NOTIMPLEMENTED is the convenience macro for returning it from a C
function. In the limited API older than 3.12 it returns a new reference; in
3.12+ it returns the immortal singleton directly:
// Include/object.h
#if defined(Py_LIMITED_API) && Py_LIMITED_API+0 < 0x030c0000
# define Py_RETURN_NOTIMPLEMENTED return Py_NewRef(Py_NotImplemented)
#else
# define Py_RETURN_NOTIMPLEMENTED return Py_NotImplemented
#endifBecause the object is in the interpreter’s constant table, it is immortal
(PEP 683) like None, True, and False:
// 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
...
};The dealloc guard
The singleton’s refcount should never reach zero, but CPython defends against accidental decref-to-zero by re-immortalizing instead of freeing:
// Objects/object.c
static void
notimplemented_dealloc(PyObject *notimplemented)
{
/* This should never get called, but we also don't want to SEGV if
* we accidentally decref NotImplemented out of existence. Instead,
* since NotImplemented is an immortal object, re-set the reference count. */
_Py_SetImmortal(notimplemented);
}This mirrors bool_dealloc and the None/Ellipsis guards: a statically
allocated immortal object has no real deallocator.
Boolean context is an error
NotImplemented is deliberately unusable as a truth value. Its tp_as_number
only fills nb_bool, and that slot raises TypeError:
// Objects/object.c
static int
notimplemented_bool(PyObject *v)
{
PyErr_SetString(PyExc_TypeError,
"NotImplemented should not be used in a boolean context");
return -1;
}
static PyNumberMethods notimplemented_as_number = {
.nb_bool = notimplemented_bool,
};So if NotImplemented:, not NotImplemented, and bool(NotImplemented) all
raise TypeError: NotImplemented should not be used in a boolean context.
This prevents a common bug where a binary operator returns NotImplemented
and the caller forgets to check for it, treating it as a truthy result.
repr and pickling
repr(NotImplemented) is the string "NotImplemented" (a fresh str, not a
cached one), and __reduce__ returns the same string so unpickling resolves
to the singleton by name:
// Objects/object.c
static PyObject *
NotImplemented_repr(PyObject *op)
{
return PyUnicode_FromString("NotImplemented");
}
static PyObject *
NotImplemented_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
{
return PyUnicode_FromString("NotImplemented");
}
static PyMethodDef notimplemented_methods[] = {
{"__reduce__", NotImplemented_reduce, METH_NOARGS, NULL},
{NULL, NULL}
};Construction: no arguments
NotImplementedType() returns the singleton; passing arguments is an error:
// Objects/object.c
static PyObject *
notimplemented_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
if (PyTuple_GET_SIZE(args) || (kwargs && PyDict_GET_SIZE(kwargs))) {
PyErr_SetString(PyExc_TypeError, "NotImplementedType takes no arguments");
return NULL;
}
Py_RETURN_NOTIMPLEMENTED;
}Note that type is ignored: there is no way to create a second instance, just
like True/False for bool.
How binary operators consume it
The whole point of the sentinel is the protocol in Objects/abstract.c. A
binary slot (e.g. nb_add) may return Py_NotImplemented to say “I don’t
know how to do this.” binary_op1 then tries the reflected/other operand’s
slot, and if everything fails, binary_op turns the sentinel into a
TypeError:
// Objects/abstract.c
static PyObject *
binary_op1(PyObject *v, PyObject *w, const int op_slot ...)
{
binaryfunc slotv = ...; /* v's slot, if any */
binaryfunc slotw = ...; /* w's slot, if types differ */
if (slotv) {
PyObject *x;
if (slotw && PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v))) {
x = slotw(v, w); /* try w first when w is a subclass */
if (x != Py_NotImplemented)
return x;
Py_DECREF(x); /* can't do it */
slotw = NULL;
}
x = slotv(v, w);
if (x != Py_NotImplemented) {
return x;
}
Py_DECREF(x); /* can't do it */
}
if (slotw) {
PyObject *x = slotw(v, w);
if (x != Py_NotImplemented) {
return x;
}
Py_DECREF(x); /* can't do it */
}
Py_RETURN_NOTIMPLEMENTED; /* nobody could do it */
}
static PyObject *
binary_op(PyObject *v, PyObject *w, const int op_slot, const char *op_name)
{
PyObject *result = BINARY_OP1(v, w, op_slot, op_name);
if (result == Py_NotImplemented) {
Py_DECREF(result);
return binop_type_error(v, w, op_name); /* TypeError */
}
return result;
}The same pattern appears in ternary_op (for pow with a modulus) and in
do_richcompare for ordering/comparison operators:
// Objects/object.c
static PyObject *
do_richcompare(PyThreadState *tstate, PyObject *v, PyObject *w, int op)
{
...
if (!Py_IS_TYPE(v, Py_TYPE(w)) &&
PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v)) &&
(f = Py_TYPE(w)->tp_richcompare) != NULL) {
checked_reverse_op = 1;
res = (*f)(w, v, _Py_SwappedOp[op]);
if (res != Py_NotImplemented)
return res;
Py_DECREF(res);
}
if ((f = Py_TYPE(v)->tp_richcompare) != NULL) {
res = (*f)(v, w, op);
if (res != Py_NotImplemented)
return res;
Py_DECREF(res);
}
if (!checked_reverse_op && (f = Py_TYPE(w)->tp_richcompare) != NULL) {
res = (*f)(w, v, _Py_SwappedOp[op]);
if (res != Py_NotImplemented)
return res;
Py_DECREF(res);
}
/* default: == / != compare by identity; ordering raises TypeError */
...
}The default object_richcompare itself uses NotImplemented as its “I have
nothing to say” return value, which is what lets two unrelated objects fall
through to identity equality for ==/!= and to a TypeError for ordering:
// Objects/typeobject.c
static PyObject *
object_richcompare(PyObject *self, PyObject *other, int op)
{
PyObject *res;
switch (op) {
case Py_EQ:
res = Py_NewRef((self == other) ? Py_True : Py_NotImplemented);
break;
case Py_NE:
...
res = Py_NewRef(Py_NotImplemented);
break;
default:
res = Py_NewRef(Py_NotImplemented);
break;
}
return res;
}The slot-wrapper machinery in typeobject.c (SLOT1BINFULL, etc.) likewise
compares returned values against Py_NotImplemented to decide whether to try
the reflected dunder (__radd__ and friends) or the other operand.
Memory layout summary
NotImplemented is just a PyObject head: refcount, flags, and type pointer.
There is no value, no digit array, no buffer:
_Py_NotImplementedStruct
+---------------------------+
| ob_refcnt (immortal) |
+---------------------------+
| ob_flags (static) |
+---------------------------+
| *ob_type (&_PyNotImplemented_Type) |
+---------------------------+The ABI records its size as 16 bytes on 64-bit builds, matching sizeof(PyObject).
Key takeaways
NotImplementedis a single immortal staticPyObject,_Py_NotImplementedStruct, of type_PyNotImplemented_Type(NotImplementedType).- The type is a subclass of
objectwith almost no slots: onlytp_repr,tp_as_number(whosenb_boolraisesTypeError),tp_methods(__reduce__), andtp_new(which takes no arguments and returns the singleton). Py_NotImplementedresolves to the struct directly in the full API, or to a borrowed reference from the constant table (Py_CONSTANT_NOT_IMPLEMENTED, index 4) in the limited API 3.13+.- The dealloc guard re-immortalizes instead of freeing.
- Binary/ternary operators and rich comparisons treat a returned
Py_NotImplementedas “try the other operand”; if nobody can handle it, the sentinel is converted into aTypeError. bool(NotImplemented)andif NotImplemented:raiseTypeErrorby construction, catching forgotten sentinel checks.
Further reading
Objects/object.c–_PyNotImplemented_Type,_Py_NotImplementedStruct,NotImplemented_repr,notimplemented_new,notimplemented_dealloc,notimplemented_bool,do_richcompare, and theconstants[]table.Include/object.h–Py_NotImplemented,Py_RETURN_NOTIMPLEMENTED, andPy_CONSTANT_NOT_IMPLEMENTED.Objects/abstract.c–binary_op1,binary_op, andternary_op.Objects/typeobject.c–object_richcompareand theSLOT1BINFULLmachinery.Modules/_typesmodule.c– export ofNotImplementedType.