Iteration

Iterator vs Iterable

Iterables produce fresh iterators; iterators are one-pass.

A list is iterable. Each for loop or list() call asks the list for a fresh iterator under the hood, so the same data can be traversed many times.

Source

names = ["Ada", "Grace"]
print(list(names))
print(list(names))

Output

['Ada', 'Grace']
['Ada', 'Grace']
ITERABLE[a,b,c]iter()ITERATORnext()abc
An iterable knows how to produce an iterator (via iter()); the iterator knows how to produce values (via next()).

An iterator is one-pass. Calling iter() returns a position-tracking object; once it has been exhausted, it stays exhausted.

Source

stream = iter(names)
print(list(stream))
print(list(stream))

Output

['Ada', 'Grace']
[]

Calling iter() on an iterable returns a brand-new iterator each time. Calling iter() on an iterator returns the same object — that is the rule that lets a for loop accept either kind.

Source

first = iter(names)
second = iter(names)
print(first is second)
print(iter(first) is first)

Output

False
True

The distinction shows up at API boundaries. A function that loops over its argument twice works for an iterable but silently produces wrong answers for an iterator, because the second pass finds the iterator already exhausted. Materialize once at the boundary when both passes matter.

Source

def total_and_count(numbers):
    total = sum(numbers)
    count = sum(1 for _ in numbers)
    return total, count

def values():
    yield from [10, 9, 8]

print(total_and_count([10, 9, 8]))
print(total_and_count(values()))

def total_and_count_safe(numbers):
    items = list(numbers)
    return sum(items), len(items)

print(total_and_count_safe(values()))

Output

(27, 3)
(27, 0)
(27, 3)

Notes

See also

Run the complete example

Example code

Expected output

['Ada', 'Grace']
['Ada', 'Grace']
['Ada', 'Grace']
[]
False
True
(27, 3)
(27, 0)
(27, 3)

Execution time appears here after you run the example.