How CPython Implements bool in C

bool as a subclass of int, the immortal True/False singletons, tp_new, truthiness, and bitwise overrides traced through the CPython 3.15 source.

Python’s bool is unusual: it is a real type, but there are exactly two instances, True and False, and they are also the integers 1 and 0. In CPython that is implemented in Objects/boolobject.c by making PyBool_Type a subclass of PyLong_Type and storing the two values as static _longobject structs. This article traces that implementation through the 3.15 source.

The type is a subclass of int

bool is not a separate representation. It reuses PyLongObject’s layout and inherits almost everything from int:

c
// Objects/boolobject.c
PyTypeObject PyBool_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "bool",
    offsetof(struct _longobject, long_value.ob_digit),  /* tp_basicsize */
    sizeof(digit),                              /* tp_itemsize */
    bool_dealloc,                               /* tp_dealloc */
    ...
    &bool_as_number,                            /* tp_as_number */
    ...
    &PyLong_Type,                               /* tp_base */
    ...
    bool_new,                                   /* tp_new */
    .tp_vectorcall = bool_vectorcall,
};

Two details matter. First, tp_base is &PyLong_Type, so bool is a real subclass of int: isinstance(True, int) is true and all of int’s methods are inherited (see below). Second, tp_flags is just Py_TPFLAGS_DEFAULT – there is no Py_TPFLAGS_BASETYPE, so bool cannot be subclassed in Python:

python
>>> class MyBool(bool): pass
TypeError: type 'bool' is not an acceptable base type

PyBool_Check is an exact type check, not a subclass check, which is why the bitwise overrides below can safely assume “this is exactly a bool”:

c
// Include/boolobject.h
#define PyBool_Check(x) Py_IS_TYPE((x), &PyBool_Type)

The two singletons

True and False are not allocated at runtime. They are static C structs of type _longobject (the same struct int uses), initialized at compile time:

c
// Objects/boolobject.c
struct _longobject _Py_FalseStruct = {
    PyObject_HEAD_INIT(&PyBool_Type)
    { .lv_tag = _PyLong_FALSE_TAG,
        { 0 }
    }
};

struct _longobject _Py_TrueStruct = {
    PyObject_HEAD_INIT(&PyBool_Type)
    { .lv_tag = _PyLong_TRUE_TAG,
        { 1 }
    }
};

The tags are defined in terms of the ordinary long tag machinery:

c
// Include/internal/pycore_long.h
#define _PyLong_FALSE_TAG TAG_FROM_SIGN_AND_SIZE(0, 0)
#define _PyLong_TRUE_TAG  TAG_FROM_SIGN_AND_SIZE(1, 1)

So False is “zero digits, zero value” and True is “one digit, value 1” – exactly the same encoding an int would use. That is why bool needs no special conversion to behave as an int: True + True == 2, True * 10 == 10, and int(True) is True.

The public macros resolve to these structs (or, in the limited API, to immortal constants):

c
// Include/boolobject.h
#define Py_False _PyObject_CAST(&_Py_FalseStruct)
#define Py_True  _PyObject_CAST(&_Py_TrueStruct)

Because they are statically allocated, they go through PyObject_HEAD_INIT, which marks them immortal (PEP 683). They also appear in the interpreter’s constant table (Py_CONSTANT_FALSE / Py_CONSTANT_TRUE), whose entries are all immortal. The bool_dealloc is a belt-and-braces guard in case a refcount ever reaches zero: it simply re-immortalizes the object instead of freeing it.

c
// Objects/boolobject.c
static void
bool_dealloc(PyObject *boolean)
{
    _Py_SetImmortal(boolean);
}

PyBool_FromLong is the single factory. It never allocates; it just picks the right singleton:

c
// Objects/boolobject.c
PyObject *PyBool_FromLong(long ok)
{
    return ok ? Py_True : Py_False;
}

tp_new: always a singleton

bool() (and bool(x)) is implemented by bool_new / bool_vectorcall. The rule is simple: unpack at most one argument, test its truthiness, and return Py_True or Py_False:

c
// Objects/boolobject.c
static PyObject *
bool_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *x = Py_False;
    long ok;

    if (!_PyArg_NoKeywords("bool", kwds))
        return NULL;
    if (!PyArg_UnpackTuple(args, "bool", 0, 1, &x))
        return NULL;
    ok = PyObject_IsTrue(x);
    if (ok < 0)
        return NULL;
    return PyBool_FromLong(ok);
}

bool_vectorcall is the faster calling convention used for bool(x); it does the same thing, defaulting to False when called with no arguments. Note that type is accepted but never used to construct a new instance – there is no way to create a third bool, which is what makes True/False the only two instances the docstring promises.

Truthiness

bool_new delegates truthiness to PyObject_IsTrue in Objects/object.c. That function has fast paths for the three singletons and otherwise consults the type’s nb_bool, then mapping length, then sequence length, defaulting to true:

c
// Objects/object.c
PyObject_IsTrue(PyObject *v)
{
    Py_ssize_t res;
    if (v == Py_True)
        return 1;
    if (v == Py_False)
        return 0;
    if (v == Py_None)
        return 0;
    else if (Py_TYPE(v)->tp_as_number != NULL &&
             Py_TYPE(v)->tp_as_number->nb_bool != NULL)
        res = (*Py_TYPE(v)->tp_as_number->nb_bool)(v);
    else if (Py_TYPE(v)->tp_as_mapping != NULL &&
             Py_TYPE(v)->tp_as_mapping->mp_length != NULL)
        res = (*Py_TYPE(v)->tp_as_mapping->mp_length)(v);
    else if (Py_TYPE(v)->tp_as_sequence != NULL &&
             Py_TYPE(v)->tp_as_sequence->sq_length != NULL)
        res = (*Py_TYPE(v)->tp_as_sequence->sq_length)(v);
    else
        return 1;
    return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}

This is also what if x:, not x, and the and/or operators use, so bool(x) and truthiness agree by construction. Note bool itself has nb_bool == 0 (left NULL so it is inherited from int); an int’s nb_bool is effectively “nonzero”, so bool(True) works through the Py_True fast path before ever reaching that.

bool as int

Because PyBool_Type is a subclass of PyLong_Type, almost all arithmetic is inherited. bool_as_number only fills in a handful of slots; the rest are copied from int by PyType_Ready (COPYNUM):

c
// Objects/boolobject.c
static PyNumberMethods bool_as_number = {
    0,                          /* nb_add */      // inherited from int
    ...
    bool_invert,                /* nb_invert */
    ...
    bool_and,                   /* nb_and */
    bool_xor,                   /* nb_xor */
    bool_or,                    /* nb_or */
    ...
};

So True + True == 2 (an int), True << 3 == 8, and so on, all use int’s implementations. Only the bitwise logical operators are overridden, and only so that &, |, ^ on two bools return a bool instead of an int:

c
// Objects/boolobject.c
static PyObject *
bool_and(PyObject *a, PyObject *b)
{
    if (!PyBool_Check(a) || !PyBool_Check(b))
        return PyLong_Type.tp_as_number->nb_and(a, b);
    return PyBool_FromLong((a == Py_True) & (b == Py_True));
}

static PyObject *
bool_or(PyObject *a, PyObject *b)
{
    if (!PyBool_Check(a) || !PyBool_Check(b))
        return PyLong_Type.tp_as_number->nb_or(a, b);
    return PyBool_FromLong((a == Py_True) | (b == Py_True));
}

static PyObject *
bool_xor(PyObject *a, PyObject *b)
{
    if (!PyBool_Check(a) || !PyBool_Check(b))
        return PyLong_Type.tp_as_number->nb_xor(a, b);
    return PyBool_FromLong((a == Py_True) ^ (b == Py_True));
}

If either operand is not exactly a bool, the call falls through to int’s bitwise operation, which returns an int. ~ is special: it is deprecated on bool (it returns the underlying int’s bitwise inversion, e.g. ~True == -2) and emits a DeprecationWarning, and will be removed in 3.16.

bool_repr returns the interned identifiers, so repr(True) == "True":

c
// Objects/boolobject.c
static PyObject *
bool_repr(PyObject *self)
{
    return self == Py_True ? &_Py_ID(True) : &_Py_ID(False);
}

Hashing and comparison are inherited from int: hash(True) == 1, True == 1 is True, and True is 1 is False – the usual identity-vs-equality distinction, since 1 is a separate (possibly cached) int object.

Memory layout summary

There is no separate allocation for bools. The two singletons are static _longobjects whose lv_tag and single digit are the values 0 and 1:

text
_Py_FalseStruct                _Py_TrueStruct
+---------------------------+  +---------------------------+
| ob_refcnt  (immortal)     |  | ob_refcnt  (immortal)     |
+---------------------------+  +---------------------------+
| *ob_type  (&PyBool_Type)  |  | *ob_type  (&PyBool_Type)  |
+---------------------------+  +---------------------------+
| lv_tag  (FALSE_TAG: 0)    |  | lv_tag  (TRUE_TAG: 1<<3)  |
+---------------------------+  +---------------------------+
| ob_digit[0] = 0           |  | ob_digit[0] = 1           |
+---------------------------+  +---------------------------+

PyBool_Type.tp_basicsize is offsetof(struct _longobject, long_value.ob_digit) and tp_itemsize is sizeof(digit), identical to int, so even the memory layout is shared with PyLongObject.

Key takeaways

  • bool is a subclass of int (PyBool_Type.tp_base == &PyLong_Type) with no Py_TPFLAGS_BASETYPE, so it cannot be subclassed.
  • True and False are two immortal static _longobjects whose tags and digits are the integers 1 and 0; PyBool_FromLong only selects between them, so no bool is ever allocated.
  • bool(x) (bool_new/bool_vectorcall) tests truthiness via PyObject_IsTrue and returns a singleton.
  • Truthiness fast-paths True/False/None, then nb_bool, mapping length, sequence length, defaulting to true.
  • All arithmetic is inherited from int; only &, |, ^ (and a deprecated ~) are overridden so bool op bool returns bool.

Further reading

  • Objects/boolobject.c – type object, singletons, tp_new, bitwise ops.
  • Include/boolobject.h – Py_True/Py_False macros and PyBool_Check.
  • Objects/object.c – PyObject_IsTrue.
  • Include/internal/pycore_long.h – _PyLong_FALSE_TAG/_PyLong_TRUE_TAG.
  • Objects/longobject.c – the inherited int implementation.

PythonCpythonInternalsBool