35.1 Infinite Iterators: count, cycle, repeat
The itertools module provides a powerful suite of tools for creating and working with iterators. Among its most conceptually intriguing tools are the infinite iterators: count, cycle, and repeat. These functions allow you to generate sequences that continue indefinitely, a capability that must be managed carefully to avoid infinite loops but is incredibly useful for generating data streams, simulating continuous processes, and pairing with termination conditions.
The count Iterator: An Endless Numerical Sequence
The itertools.count() function generates an arithmetic progression of numbers indefinitely. It is the programmable equivalent of counting aloud forever. Its signature is count(start=0, step=1), where start defines the first number in the sequence and step defines the difference between each subsequent number.
Why it works this way: count leverages the simplicity of an arithmetic sequence. Each value is calculated on-demand as start + n * step, where n is the number of times the iterator has been advanced. This makes it incredibly memory-efficient; it doesn’t precompute or store the entire infinite sequence, only the current state (the next value to yield).
A common use case is generating unique identifiers or indices for an enumerating operation that doesn’t know the total length of the data it’s processing beforehand.
import itertools
# Generate a sequence of even numbers starting from 10
even_counter = itertools.count(start=10, step=2)
# Print the first 5 even numbers from the sequence
for i, num in enumerate(even_counter):
if i >= 5:
break
print(num)
Output:
10
12
14
16
18
A critical pitfall is forgetting to build in a termination condition. Using count() directly in a for loop without a break statement or combining it with a finite iterator (like zip) will result in an infinite loop that will run until interrupted or until the numbers become impractically large. For example, using count() with a step of 0 will yield the same value endlessly, which is rarely useful.
The cycle Iterator: Looping Over a Finite Sequence Infinitely
The itertools.cycle(iterable) function takes a finite input iterable and returns an iterator that produces items from it. Once the iterable is exhausted, it restarts from the beginning, repeating this process infinitely.
Why it works this way: Internally, cycle works by consuming the entire input iterable and saving a copy of its elements in an internal data structure (like a tuple). Once this buffer is created, it can yield items from this buffer repeatedly without needing to reference the original iterable again. This is why it’s crucial that the input iterable is finite; an infinite input would prevent cycle from ever completing its initial buffering phase.
This is ideal for tasks like alternating between statuses, cycling through a fixed set of colors for data visualization, or simulating a circular buffer.
import itertools
status_light = itertools.cycle(['Red', 'Yellow', 'Green'])
# Simulate 10 cycles of the traffic light
for _ in range(10):
print(next(status_light))
Output:
Red
Yellow
Green
Red
Yellow
Green
Red
Yellow
Green
Red
The major pitfall with cycle is its memory consumption. Because it must create an internal copy of the entire input sequence, using cycle on a very large or memory-intensive iterable (e.g., a list of massive objects) can be expensive. It’s best suited for small, finite sequences. Furthermore, if the input iterable is empty, cycle will correctly produce an empty, non-infinite iterator, yielding nothing.
The repeat Iterator: Repeating a Single Value
The itertools.repeat(object[, times]) function returns an iterator that produces the same value over and over again. If the optional times argument is provided, it will repeat the value exactly that number of times. If times is omitted, the iterator runs indefinitely.
Why it works this way: The function is a minimalist workhorse. It doesn’t need to perform any calculation or maintain a buffer; it simply yields a reference to the same stored object repeatedly. This makes it extremely efficient for generating a constant value stream.
A quintessential use case is providing a constant fill value to zip_longest. It’s also useful for creating a stream of default values or for supplying a fixed parameter to a function called repeatedly with map.
import itertools
# Example 1: Repeat a value a finite number of times
siren_sound = itertools.repeat('Wee-Ooh', times=3)
for sound in siren_sound:
print(sound)
# Example 2: Use with zip to provide a constant value for a matrix column
data_rows = [[1, 2], [3, 4], [5, 6]]
default_column = itertools.repeat(0) # Infinite source of zeros
# Add a third column of zeros to each row
augmented_rows = [row + [next(default_column)] for row in data_rows]
print(augmented_rows)
# Example 3: Use with map to raise numbers to a fixed power (e.g., square)
numbers = [1, 2, 3, 4]
squares = list(map(pow, numbers, itertools.repeat(2)))
print(squares)
Output:
Wee-Ooh
Wee-Ooh
Wee-Ooh
[[1, 2, 0], [3, 4, 0], [5, 6, 0]]
[1, 4, 9, 16]
The primary pitfall involves using repeat with a mutable object. Since the same object is yielded every time, any in-place modification of that object will affect all future yields. This is often unintended.
import itertools
# Pitfall: Repeating a mutable list
list_repeater = itertools.repeat(['A', 'B'])
first_list = next(list_repeater)
first_list.append('C') # Modifies the internal stored list
second_list = next(list_repeater)
print(second_list) # Output: ['A', 'B', 'C'] - This is probably a bug!
Best Practice: To avoid this, ensure you are repeating an immutable object (like an integer, string, or tuple) or be acutely aware of the mutability side effects. If you need independent mutable copies, use a generator expression like (item.copy() for item in repeat(mutable_object)) or combine repeat with map and a copy function.