Python and many other programming languages provide a unified way to process elements of a container value sequentially, called an iterator. An iterator is an object that provides sequential access to values, one by one.

The iterator abstraction has two components: a mechanism for retrieving the next element in the sequence being processed and a mechanism for signaling that the end of the sequence has been reached and no further elements remain. For any container, such as a list or range, an iterator can be obtained by calling the built-in iter function. The contents of the iterator can be accessed by calling the built-in next function.

>>> primes = [2, 3, 5, 7]
    >>> type(primes)
    >>> iterator = iter(primes)
    >>> type(iterator)
    >>> next(iterator)
    2
    >>> next(iterator)
    3
    >>> next(iterator)
    5
    

The way that Python signals that there are no more values available is to raise a StopIteration exception when next is called. This exception can be handled using a try statement.

>>> next(iterator)
    7
    >>> next(iterator)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    >>> try:
            next(iterator)
        except StopIteration:
            print('No more values')
    No more values
    

An iterator maintains local state to represent its position in a sequence. Each time next is called, that position advances. Two separate iterators can track two different positions in the same sequence. However, two names for the same iterator will share a position, because they share the same value.

>>> r = range(3, 13)
    >>> s = iter(r)  # 1st iterator over r
    >>> next(s)
    3
    >>> next(s)
    4
    >>> t = iter(r)  # 2nd iterator over r
    >>> next(t)
    3
    >>> next(t)
    4
    >>> u = t        # Alternate name for the 2nd iterator
    >>> next(u)
    5
    >>> next(u)
    6
    

Advancing the second iterator does not affect the first. Since the last value returned from the first iterator was 4, it is positioned to return 5 next. On the other hand, the second iterator is positioned to return 7 next.

>>> next(s)
    5
    >>> next(t)
    7
    

Calling iter on an iterator will return that iterator, not a copy. This behavior is included in Python so that a programmer can call iter on a value to get an iterator without having to worry about whether it is an iterator or a container.

>>> v = iter(t)  # Another alterante name for the 2nd iterator
    >>> next(v)
    8
    >>> next(u)
    9
    >>> next(t)
    10
    

The usefulness of iterators is derived from the fact that the underlying series of data for an iterator may not be represented explicitly in memory. An iterator provides a mechanism for considering each of a series of values in turn, but all of those elements do not need to be stored simultaneously. Instead, when the next element is requested from an iterator, that element may be computed on demand instead of being retrieved from an existing memory source.

Ranges are able to compute the elements of a sequence lazily because the sequence represented is uniform, and any element is easy to compute from the starting and ending bounds of the range. Iterators allow for lazy generation of a much broader class of underlying sequential datasets, because they do not need to provide access to arbitrary elements of the underlying series. Instead, iterators are only required to compute the next element of the series, in order, each time another element is requested. While not as flexible as accessing arbitrary elements of a sequence (called random access), sequential access to sequential data is often sufficient for data processing applications.