Back to Blog
Python

Python itertools cycle: Repeat Iterables Indefinitely

python itertools cycle: Learn how itertools.cycle repeats iterables indefinitely in Python, including memory behavior, round-robin patterns, common pitfalls, and alter...

itertoolsiteratorsgeneratorsround-robinpython standard library
An illustration of a circular arrow looping through a sequence of colored elements, representing itertools.cycle repeating an iterable indefinitely.

python itertools cycle provides an iterator that repeats an iterable's elements indefinitely. When you need to rotate through a fixed set of values — load balancer backends, alternating database roles, color palettes, retry states — cycle removes the manual index-reset logic that a hand-written loop would require.

What itertools.cycle Does

itertools.cycle(iterable) returns an iterator that yields each element from the input in order, then starts over from the beginning. The sequence never terminates on its own.

from itertools import cycle colors = cycle(["red", "green", "blue"]) for _ in range(7): print(next(colors))

Output:

red
green
blue
red
green
blue
red

The input can be any iterable — a list, tuple, string, or generator — though the behavior differs depending on which you pass. The iterator produced by cycle has no length hint, so functions like list() or len() cannot determine how many elements remain; calling list(cycle(...)) will run forever.

How cycle Stores Its Input

When you pass a list or tuple to cycle, the function saves a copy of the elements internally. It does this by calling iter() on the input, then caching each element as it is consumed. This means the original collection can be modified after cycle is created without affecting the cycle's output — the cached copy is independent.

Passing a generator changes the behavior. A generator is consumed once, so cycle caches each value as it is produced. If the generator is infinite, cycle never finishes caching and the cycle itself becomes an infinite loop that also consumes unbounded memory. For finite generators, cycle works normally, but the elements are only materialized once they are first requested.

This distinction matters when you build a cycle over an expensive or stateful generator. The first pass through the sequence triggers the generator's work; subsequent passes reuse the cached values.

Practical Patterns Using cycle

Round-robin assignment is the most common use. Suppose you have three worker queues and need to distribute tasks evenly:

from itertools import cycle workers = cycle(["worker-a", "worker-b", "worker-c"]) for task_id in range(10): print(f"task {task_id} -> {next(workers)}")

This produces a deterministic rotation: task 0 goes to worker-a, task 1 to worker-b, task 2 to worker-c, task 3 back to worker-a. No counter or modulo arithmetic is needed.

Another pattern is alternating between two states, such as toggling a flag for each iteration:

from itertools import cycle toggle = cycle([True, False]) for _ in range(6): print(next(toggle))

This is clearer than tracking a boolean and flipping it manually, and it extends naturally to more than two states.

Common Mistakes When Using cycle

The most frequent error is forgetting that cycle never terminates. Any for loop over a cycle requires an explicit break condition:

from itertools import cycle for color in cycle(["red", "green"]): print(color) # runs forever

If the break condition depends on the element value, make sure it can actually be reached. A cycle over a fixed set will eventually revisit every element, so a break based on element identity works, but a break based on a counter is more predictable.

Another mistake is passing a mutable collection and expecting cycle to reflect later changes. Because cycle caches the elements at consumption time, appending to the original list after creating the cycle does not add the new element to the rotation.

Alternatives to cycle

Modulo indexing is the standard alternative when you already have an index and want to avoid creating a new iterator:

colors = ["red", "green", "blue"] for i in range(10): print(colors[i % len(colors)])

This works when the sequence is fixed and the index is already available. It does not require importing itertools and is slightly more explicit about the rotation logic. The tradeoff is that you must manage the index and the length yourself, and the expression i % len(colors) is repeated at each use.

itertools.repeat is a different tool: it yields the same value forever rather than rotating through a sequence. Use repeat when you need a constant, not a cycle.

A custom generator function gives you full control over state, which is useful when the rotation needs to skip elements or depend on external conditions:

def rotating(sequence): i = 0 while True: yield sequence[i % len(sequence)] i += 1

This is more code than cycle, but it allows inserting logic before each yield.

Performance and Memory Considerations

cycle has a fixed per-element cost: it advances an internal index and returns the cached value. There is no re-iteration of the input after the first pass, so repeated rotation does not re-run the original iterable.

The memory cost is proportional to the number of distinct elements in the input. Each element is stored once in the internal cache. For a small fixed set — a few workers, a few colors — this is negligible. For a large sequence, the cache is a full copy, so be deliberate about passing large collections.

The main performance risk is not the cycle itself but the loop that consumes it. An unbounded loop over cycle will spin at full speed and never release the GIL between iterations if the body does no I/O or sleeping. In a long-running service, this can starve other threads. If the rotation is meant to pace work, add an explicit delay or a bounded iteration count.

Compatibility and Edge Cases

itertools.cycle has been part of the standard library since Python 2 and remains unchanged in Python 3. There is no version-specific behavior to account for in current Python releases.

Passing an empty iterable raises StopIteration on the first next() call, because there are no elements to cache. This is the one edge case where cycle fails immediately rather than looping.

The function returns an iterator, not a list, so it cannot be indexed or sliced. If you need the nth element of the rotation, either advance the iterator n times or use modulo indexing on the original sequence instead. For most rotation needs, cycle is the more direct expression of the intent, and the alternatives above cover the cases where it does not fit.

python itertools cycle: Repeat Iterables Indefinitely | RYUSLOG DEV