Search code examples
pythonsettruthiness

Truth value of empty set


I am interested in the truth value of Python sets like {'a', 'b'}, or the empty set set() (which is not the same as the empty dictionary {}). In particular, I would like to know whether bool(my_set) is False if and only if the set my_set is empty.

Ignoring primitive (such as numerals) as well as user-defined types, https://docs.python.org/3/library/stdtypes.html#truth says:

The following values are considered false:

  • [...]
  • any empty sequence, for example, '', (), [].
  • any empty mapping, for example, {}.
  • [...]

All other values are considered true

According to https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range, a set is not a sequence (it is unordered, its elements do not have indices, etc.):

There are three basic sequence types: lists, tuples, and range objects.

And, according to https://docs.python.org/3/library/stdtypes.html#mapping-types-dict,

There is currently only one standard mapping type, the dictionary.

So, as far as I understand, the set type is not a type that can ever be False. However, when I try, bool(set()) evaluates to False.

Questions:

  • Is this a documentation problem, or am I getting something wrong?
  • Is the empty set the only set whose truth value is False?

Solution

  • After looking at the source code for CPython, I would guess this is a documentation error, however, it could be implementation dependent and therefore would be a good issue to raise on the Python bug tracker.

    Specifically, object.c defines the truth value of an item as follows:

    int
    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 (v->ob_type->tp_as_number != NULL &&
                 v->ob_type->tp_as_number->nb_bool != NULL)
            res = (*v->ob_type->tp_as_number->nb_bool)(v);
        else if (v->ob_type->tp_as_mapping != NULL &&
                 v->ob_type->tp_as_mapping->mp_length != NULL)
            res = (*v->ob_type->tp_as_mapping->mp_length)(v);
        else if (v->ob_type->tp_as_sequence != NULL &&
                 v->ob_type->tp_as_sequence->sq_length != NULL)
            res = (*v->ob_type->tp_as_sequence->sq_length)(v);
        else
            return 1;
        /* if it is negative, it should be either -1 or -2 */
        return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
    }
    

    We can clearly see that the value is value would be always true if it is not a boolean type, None, a sequence, or a mapping type, which would require tp_as_sequence or tp_as_mapping to be set.

    Fortunately, looking at setobject.c shows that sets do implement tp_as_sequence, suggesting the documentation seems to be incorrect.

    PyTypeObject PySet_Type = {
        PyVarObject_HEAD_INIT(&PyType_Type, 0)
        "set",                              /* tp_name */
        sizeof(PySetObject),                /* tp_basicsize */
        0,                                  /* tp_itemsize */
        /* methods */
        (destructor)set_dealloc,            /* tp_dealloc */
        0,                                  /* tp_print */
        0,                                  /* tp_getattr */
        0,                                  /* tp_setattr */
        0,                                  /* tp_reserved */
        (reprfunc)set_repr,                 /* tp_repr */
        &set_as_number,                     /* tp_as_number */
        &set_as_sequence,                   /* tp_as_sequence */
        0,                                  /* tp_as_mapping */
        /* ellipsed lines */
    };
    

    Dicts also implement tp_as_sequence, so it seems that although it is not a sequence type, it sequence-like, enough to be truthy.

    In my opionion, the documentation should clarify this: mapping-like types, or sequence-like types will be truthy dependent on their length.

    Edit As user2357112 correctly points out, tp_as_sequence and tp_as_mapping do not mean the type is a sequence or a map. For example, dict implements tp_as_sequence, and list implements tp_as_mapping.