itertools.cycle Freezes the First Lap, Replays It Forever (tested)
itertools.cycle() sounds like it repeatedly re-runs the iterable you give it, lap after lap. We measured what it actually does with a generator that produces different values each time it runs, and cycle() only ever consumes it once.
1. A generator's side effects only run on the first lap
import itertools
calls = []
def gen():
for i in range(3):
calls.append(i)
yield i
c = itertools.cycle(gen())
result = list(itertools.islice(c, 9)) # 3 full laps
print("output: ", result)
print("gen() ran for:", calls)
output: [0, 1, 2, 0, 1, 2, 0, 1, 2]
gen() ran for: [0, 1, 2]
Measured: the output does repeat [0, 1, 2] three times, but calls only ever has three entries -- gen()'s body executed exactly once, not three times. cycle() reads the generator to exhaustion a single time, caches every value it produced, then serves later laps straight from that cache.
2. A "random" generator gives the exact same values on every lap
import itertools, random
random.seed(0)
def random_gen():
for _ in range(3):
yield random.randint(1, 100)
c = itertools.cycle(random_gen())
print(list(itertools.islice(c, 9)))
[50, 98, 54, 50, 98, 54, 50, 98, 54]
Measured: the three "random" numbers from the first lap -- 50, 98, 54 -- repeat identically forever. Anyone expecting cycle() to call random_gen() fresh each time around (giving nine different numbers) gets three numbers locked in place instead, with no error or warning that it happened.
3. Passing a function instead of a generator object fixes it
import itertools, random
random.seed(0)
def random_gen():
for _ in range(3):
yield random.randint(1, 100)
def fresh_forever():
while True:
yield from random_gen() # re-created and re-run every lap
f = fresh_forever()
print(list(itertools.islice(f, 9)))
[50, 98, 54, 6, 34, 66, 63, 52, 39]
Measured: wrapping the generator in a while True loop that calls random_gen() fresh each pass gives nine distinct numbers, because each lap re-runs the generator function from scratch instead of asking cycle() to replay a cache. This is a different tool for a different need: use it when you actually want the source re-evaluated every lap; use plain itertools.cycle() when repeating the same fixed sequence is exactly what you want.
Frequently asked questions
Does itertools.cycle re-run my generator every lap?
No -- we measured this: cycle() consumes the generator to exhaustion exactly once, caches the values it produced, and replays that fixed cache on every subsequent lap. The generator's code never runs again.
Why does my 'random' generator give the same values every time inside itertools.cycle?
Because cycle() only pulls from the generator on the first pass. Whatever values came out that first time -- random or not -- are cached and repeated forever, so a random-looking first lap becomes a fixed, repeating pattern.
How do I make itertools.cycle re-evaluate the source on every lap?
You can't with cycle() itself -- it's built to cache. Instead, wrap the generator function in an infinite loop (while True: yield from your_generator()) so each lap calls the function fresh rather than relying on cycle()'s cache.
Does this caching behavior apply to a plain list passed to itertools.cycle?
Yes, in effect -- a list is already a fixed sequence of values, so cycling it was always going to repeat the same items. The surprise is specific to generators and other one-shot iterables, where people expect re-execution but get a cache instead.
Is itertools.cycle's caching documented?
The Python docs describe cycle() as saving a copy of each element as it's produced, which implies the caching, but they don't call out the random-generator or side-effect implications directly -- it's easy to miss until you measure it, as we did here.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in itertools module: passed a generator (with and without an internal random.randint() call, seeded for reproducibility) to itertools.cycle(), then pulled multiple full laps with itertools.islice() and recorded both the output and how many times the generator's body actually ran. PlainHeaders is our extension; Python's itertools module is built in.