How does range object allow for multiple iterations while generators object do not?

A range object is a plain iterable sequence, while a generator is also an iterator.

The difference between the two is that an iterable is used to generate iterators which store the iteration state. This can be seen if we play around with range, its iterators, and next a bit.

First, we can see that range is not an iterator if we try to call next on it

In [1]: next(range(0))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [1], in <module>
----> 1 next(range(0))

TypeError: 'range' object is not an iterator

We can create the iterator ourselves by calling the iter builtin, we can see that this gives us a different iterator type when called on our range.

In [2]: iter(range(0))
Out[2]: <range_iterator at 0x28573eabc90>

Each of the iterators created by the iterable will store its own iteration state (say, an index into the range object that's incremented every time it's advanced) so we can use them independently

In [3]: range_obj = range(10)

In [4]: iterator_1 = iter(range_obj)

In [5]: iterator_2 = iter(range_obj)

In [6]: [next(iterator_1) for _ in range(5)]  # advance iterator_1 5 times
Out[6]: [0, 1, 2, 3, 4]

In [7]: next(iterator_2)  # left unchanged, fetches first item from range_obj
Out[7]: 0

Python also creates iterators by itself when a for loop is used, which can be seen if we take a look at instructions generator for it

In [8]: dis.dis("for a in b: ...")
  1           0 LOAD_NAME                0 (b)
              2 GET_ITER
        >>    4 FOR_ITER                 4 (to 10)
              6 STORE_NAME               1 (a)
              8 JUMP_ABSOLUTE            4
        >>   10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

Here, the GET_ITER is the same as doing iter(b).

Now with the generator, after creating it by calling the generator function, Python gives you an iterator directly, as there's no iterable object above it to be generated from. Calling the generator function could be seen as calling iter(...), but passing it everything is left up to the user as arguments to the function instead of fetching the information from an object it was created by.