Building Pipelines with itertools

Ready-made parts for working with sequences

Advanced 10 min

In this lesson

itertools is a standard-library module full of small, sharp tools for working with sequences. Every one of them returns an iterator, so they slot together into pipelines that process data without ever building the whole thing in memory.

Explain it like I’m 5

itertools is a drawer of pre-made pipe fittings. You are connecting parts, not machining your own.

The ones you will actually use

The module has around twenty functions. These five cover nearly everything a beginner needs:

  • chain(a, b), treat several sequences as one continuous run.
  • islice(source, n), take the first n items, including from something endless.
  • count(start), an endless counter; cycle(items), repeat forever.
  • groupby(items, key), group neighboring items that share a key.
  • zip_longest(a, b), pair up sequences of different lengths.
Example
from itertools import chain, islice, count

first = ["a", "b", "c"]
second = ["d", "e", "f"]

print(list(chain(first, second)))
print(list(islice(chain(first, second), 4)))

# islice is what makes an endless source safe to use:
print(list(islice(count(10), 5)))
Output
['a', 'b', 'c', 'd', 'e', 'f']
['a', 'b', 'c', 'd']
[10, 11, 12, 13, 14]
chain joins; islice takes only what you asked for.

groupby needs sorted data

groupby collects neighboring items that share a key. It does not search the whole sequence, which makes it fast and lazy, and means unsorted input produces fragmented groups.

So the rule is: sort by the same key first. This catches everyone once.

Example
from itertools import groupby

books = [
    {"genre": "scifi", "title": "Dune"},
    {"genre": "crime", "title": "Gorky Park"},
    {"genre": "scifi", "title": "Neuromancer"},
]

get_genre = lambda book: book["genre"]

print("unsorted:")
for genre, group in groupby(books, key=get_genre):
    print(" ", genre, [b["title"] for b in group])

print("sorted first:")
for genre, group in groupby(sorted(books, key=get_genre), key=get_genre):
    print(" ", genre, [b["title"] for b in group])
Output
unsorted:
  scifi ['Dune']
  crime ['Gorky Park']
  scifi ['Neuromancer']
sorted first:
  crime ['Gorky Park']
  scifi ['Dune', 'Neuromancer']
The same data, grouped twice. Only one is useful.

Chaining parts into a pipeline

Because every one of these returns an iterator, you can feed one straight into the next. Nothing is computed until something actually consumes the result, and even then only one item at a time.

The practical effect: a pipeline over a million-row file uses the same memory as one over ten rows.

Example
from itertools import chain, islice

batch_one = range(1, 5)
batch_two = range(100, 105)

# Nothing runs yet; this just describes the work.
pipeline = (n * 2 for n in chain(batch_one, batch_two) if n % 2 == 0)

print(list(islice(pipeline, 3)))
Output
[4, 8, 200]
Join, filter, transform, take three. One pass, no lists built.

Combine two lists and take the first five items, without building a merged list. Use chain to join them and islice to take five.

from itertools import chain, islice

first = ["a", "b", "c"]
second = ["d", "e", "f"]

# TODO: take the first five items across both lists
for item in []:
    print(item)

Common mistake: Calling groupby on unsorted data

Why it happens:

It looks like it should find every matching item wherever it is.

How to fix it:

It only groups neighbors. Sort by the same key first, and use the same key function for both.

Common mistake: Wrapping the pipeline in list() too early

Why it happens:

list() makes it printable, so it gets added while debugging and left behind.

How to fix it:

That materializes everything and throws away the memory benefit. Keep it lazy until the final step.

Common mistake: Iterating count() or cycle() with no limit

Why it happens:

They look like ordinary sequences.

How to fix it:

They never end. Always pair them with islice or a break.

What must be true of your data before using groupby?

What does islice(count(10), 5) produce?

Why is a chained pipeline memory-efficient?

Mini exercise (medium)

You have two lists of log lines. Write first_errors(a, b, limit) that treats them as one sequence, keeps only the lines containing "ERROR", and returns at most limit of them, without building a combined list.

Give it a shot. Complete the code and press Run; there’s nothing to download or configure.

from itertools import chain, islice

batch_a = ["INFO ok", "ERROR disk full", "INFO done"]
batch_b = ["ERROR timeout", "INFO idle", "ERROR refused"]

def first_errors(a, b, limit):
    return []  # TODO: chain the sources, keep ERROR lines, cap at limit

print(first_errors(batch_a, batch_b, 2))
print(first_errors(batch_a, batch_b, 10))

What to learn next

You met the pieces worth knowing: chain, islice, count, cycle, groupby, and zip_longest. You learned that groupby only groups neighbors so the data must be sorted first, and built a pipeline that stays lazy until something consumes it.

That was repetition removed from your data handling. Decorators removes it from your functions: write the wrapping once, apply it with one line.