Why bool((i for i in range(0))) is True

Hello! Recently I’ve found that bool(“AN EMPTY GENERATOR”) is True.

code : print(bool(range(0)), bool((i for i in range(0))), bool(list((i for i in range(0)))))
output: False True False

I think it’s a little bit weired, just like bool(“AN EMPTY LIST”) is False, what do you think ?In addition, I’ve tried calling next() on an “empty” generator, it raised StopIteration exception.

Generators are true even if they turn out to be empty because you can’t tell whether it’s empty or not until you try iterating over it, and objects are, by default, true.

2 Likes

Thanks for your reply, so if I want to know whether a generator empty or not, I have to cast it to a list first.Or is there any better way to do this?

If the only thing you want to know is “is this empty”, you can probe it for just one element - if you get one, it’s not empty:

try: next(gen)
except StopIteration: print("It's empty")

But this is a destructive operation, and you will lose the first element.

1 Like

I see, thank you.

Just opining, but maybe it would be useful for a generator supported a push operation which pushes a value onto the front of the generator. That value would be returned before actually touching the underlying generator.

Trivial half-assed implementation of the idea:

class gen2:
    def __init__(self, gen1):
        self.pushes = []
        self.gen1 = gen1

    def __iter__(self):
        return self

    def push(self, val):
        self.pushes.insert(0, val)

    def __next__(self):
        if self.pushes:
            return self.pushes.pop(0)
        return next(self.gen1)

x = gen2(i for i in range(10))
x.push(-1)
print(next(x))
print(next(x))
x.push(42)
for i in x:
    print(">>", i)

When run, it outputs:

-1
0
>> 42
>> 1
>> 2
>> 3
>> 4
>> 5
>> 6
>> 7
>> 8
>> 9
1 Like

Hi Skip, Inspired by your code.My idea is to create a copy of the original generator and then, when calling the __bool__ function, use the copy to determine if it’s empty.Here is the half-assed code:

import itertools
class gen3:
    def __init__(self, gen1):
        self.gen1, self._gen2 = itertools.tee(gen1, 2)
        self._empty = False
    def __iter__(self):
        return self

    def __next__(self):
        r = next(self.gen1, False)
        if r is not False:
            return r
        self._empty = True
        raise StopIteration

    def __bool__(self):
        if self._empty:
            return False
        try:
            next(self._gen2)
            return True
        except StopIteration:
            return False

x = gen3(i for i in range(10))
print(bool(x))
for i in x:
    print(i)
print(bool(x))
x1 = gen3(i for i in range(0))
print(bool(x1))

outputs:

True
0
1
2
3
4
5
6
7
8
9
False
False

refs:

Optimization:

# removed self._empty
import itertools
class gen3:
    def __init__(self, gen1):
        self.gen1, self._gen2 = itertools.tee(gen1, 2)

    def __iter__(self):
        return self

    def __next__(self):
        r = next(self.gen1, False)
        if r is not False:
            return r
        self._gen2 = self.gen1
        raise StopIteration

    def __bool__(self):
        try:
            next(self._gen2)
            return True
        except StopIteration:
            return False


x = gen3(i for i in range(10))
print(bool(x))
for i in x:
    print(i)
print(bool(x))
x1 = gen3(i for i in range(0))
print(bool(x1))

output is the same.

1 Like

more_itertools.peekable has __bool__ working as you desire (and has prepend and peeking).

3 Likes

Thanks, I’ve tried more_itertools.peekable , it works!

1 Like

Shows the lispy roots. Basically nil and empty list are false and False is false and 0 is false. Pretty much everything else is true. It is convenient mostly that it is so.

1 Like

For a succinct accounting of what is considered false during testing for truth value, we can find official documentation at Truth Value Testing. As stated, that would include, among other things, empty sequences and collections such as '', (), [], {}, set(), and range(0).

According to your answer, I’d like to post the source code I found in Cpython here.

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 (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;
    /* if it is negative, it should be either -1 or -2 */
    return (res > 0) ? 1 : Py_SAFE_DOWNCAST(res, Py_ssize_t, int);
}