If you have ever written a deeply nested loop just to combine two lists, or reached for a manual counter inside a while loop, python itertools is about to become your new favourite module. It ships with every Python installation, costs you nothing, and replaces a surprising amount of hand-rolled iteration logic with clean, readable one-liners.

The itertools module python documentation describes it as "functions creating iterators for efficient looping." That is a polite way of saying: stop writing loops by hand when Python already has a better answer.

Throughout this guide you will see exactly when to reach for each tool, how it behaves, and what the output looks like — no fluff, just working code.


What Makes Python Itertools Worth Learning

Before diving into individual functions, it helps to understand one key idea: every function in the itertools module returns an iterator, not a list. That means values are produced one at a time, on demand. You never pay the memory cost of building the entire result upfront.

import itertools

# This does NOT build a list of a million numbers in memory
counter = itertools.count(start=1)
print(next(counter)) # 1
print(next(counter)) # 2

That lazy evaluation is especially valuable when working with large datasets or combining multiple sequences together.


Infinite Iterators — count, cycle, and repeat

Python itertools includes three iterators that never stop producing values on their own. You pair them with something like islice or a break condition to control how many values you actually consume.

itertools.count

count(start, step) produces an endless stream of numbers beginning at start, incrementing by step. Think of it as range() that never ends.

import itertools

for n in itertools.count(10, 5):
 if n > 35:
 break
 print(n)
10
15
20
25
30
35

This is useful any time you need to number items in a stream without knowing the total length in advance — for example, tagging lines as they arrive from a file or a socket.

itertools.cycle

cycle(iterable) loops through a sequence forever. Once it reaches the end, it wraps back around to the beginning.

import itertools

colors = itertools.cycle(["red", "green", "blue"])
for i, color in zip(range(6), colors):
 print(f"Item {i}: {color}")
Item 0: red
Item 1: green
Item 2: blue
Item 3: red
Item 4: green
Item 5: blue

A practical use case: assigning round-robin labels, rotating API keys, or alternating row styles in a report.

itertools.repeat

repeat(value, times) emits the same value over and over. If you omit times, it runs forever.

import itertools

for x in itertools.repeat("hello", 3):
 print(x)
hello
hello
hello

repeat is often paired with map or zip when you need to broadcast a single value alongside a sequence.


Slicing and Filtering Iterators — islice and compress

itertools.islice

islice(iterable, stop) or islice(iterable, start, stop, step) lets you slice any iterator the same way you would slice a list — except it works on things that do not support indexing.

import itertools

# Take only the first 4 values from an infinite counter
first_four = list(itertools.islice(itertools.count(100), 4))
print(first_four)
[100, 101, 102, 103]

This is how you safely consume from any of the python infinite iterators shown above. Without islice, list(itertools.count(100)) would run until your machine ran out of memory.

itertools.compress

compress(data, selectors) filters a sequence by pairing each element with a corresponding boolean selector. When the selector is truthy, the element passes through; when it is falsy, the element is dropped.

import itertools

fruits = ["apple", "banana", "cherry", "date", "elderberry"]
in_stock = [1, 0, 1, 0, 1]

available = list(itertools.compress(fruits, in_stock))
print(available)
['apple', 'cherry', 'elderberry']

compress is cleaner than a list comprehension with an index whenever your mask already exists as a separate sequence — for example, boolean columns from a data pipeline.


Chaining Sequences Together — itertools.chain

chain(*iterables) connects multiple sequences end-to-end and treats them as one continuous stream. You do not need to copy data into a new combined list first.

import itertools

letters = ["a", "b", "c"]
numbers = [1, 2, 3]
symbols = ["!", "@", "#"]

combined = list(itertools.chain(letters, numbers, symbols))
print(combined)
['a', 'b', 'c', 1, 2, 3, '!', '@', '#']

There is also chain.from_iterable(nested) which unpacks one level of nesting for you:

import itertools

nested = [["cat", "dog"], ["fish", "bird"], ["hamster"]]
flat = list(itertools.chain.from_iterable(nested))
print(flat)
['cat', 'dog', 'fish', 'bird', 'hamster']

Python itertools chain is one of the most frequently used tools when merging results from multiple queries, files, or API pages into a single pass.


Combinatoric Iterators — combinations, permutations, and product

This is where python itertools really shines for data science, puzzle solving, and algorithm problems. These three functions generate every possible arrangement or selection from your inputs.

itertools.combinations

combinations(iterable, r) picks r items at a time from the input, without repetition and without caring about order. ("A", "B", "C") and ("C", "A", "B") count as the same combination.

import itertools

players = ["Alice", "Bob", "Carol", "Dave"]
pairs = list(itertools.combinations(players, 2))
for pair in pairs:
 print(pair)
('Alice', 'Bob')
('Alice', 'Carol')
('Alice', 'Dave')
('Bob', 'Carol')
('Bob', 'Dave')
('Carol', 'Dave')

Six pairs from four players — that is the classic "how many handshakes" problem solved in two lines.

itertools.permutations

permutations(iterable, r) does the same thing but order matters. ("A", "B") and ("B", "A") are counted separately.

import itertools

podium = list(itertools.permutations(["Gold", "Silver", "Bronze"], 2))
for place in podium:
 print(place)
('Gold', 'Silver')
('Gold', 'Bronze')
('Silver', 'Gold')
('Silver', 'Bronze')
('Bronze', 'Gold')
('Bronze', 'Silver')

Use permutations any time sequence matters — seating arrangements, password generation, route planning.

itertools.product

product(*iterables, repeat=1) is the Cartesian product — every combination drawn one item from each iterable. This is equivalent to nested for loops but written in a single readable expression.

import itertools

sizes = ["S", "M", "L"]
colors = ["Red", "Blue"]

variants = list(itertools.product(sizes, colors))
for v in variants:
 print(v)
('S', 'Red')
('S', 'Blue')
('M', 'Red')
('M', 'Blue')
('L', 'Red')
('L', 'Blue')

The repeat parameter is handy when you want the same iterable crossed with itself multiple times — for example, product("AB", repeat=2) gives all two-character strings from A and B.


Grouping Data — itertools.groupby

groupby(iterable, key) scans a sequence and groups consecutive elements that share the same key value. The key point — and the most common mistake — is that the input must be sorted by the same key first, otherwise non-adjacent equal groups are treated as separate groups.

import itertools

transactions = [
 {"date": "2024-01-01", "amount": 50},
 {"date": "2024-01-01", "amount": 30},
 {"date": "2024-01-02", "amount": 20},
 {"date": "2024-01-03", "amount": 80},
 {"date": "2024-01-03", "amount": 45},
]

# Sort first, then group
transactions.sort(key=lambda x: x["date"])

for date, group in itertools.groupby(transactions, key=lambda x: x["date"]):
 total = sum(t["amount"] for t in group)
 print(f"{date}: ${total}")
2024-01-01: $80
2024-01-02: $20
2024-01-03: $125

itertools groupby python is the go-to tool for aggregating sorted data streams — log files, sorted CSV exports, or any pre-ordered query result.


Accumulate — Running Totals Made Easy

accumulate(iterable, func) applies a function cumulatively across a sequence and yields each intermediate result. By default the function is addition, giving you a running total.

import itertools
import operator

sales = [120, 85, 200, 95, 160]

running_total = list(itertools.accumulate(sales))
print("Running total:", running_total)

running_max = list(itertools.accumulate(sales, func=max))
print("Running max: ", running_max)
Running total: [120, 205, 405, 500, 660]
Running max: [120, 120, 200, 200, 200]

Any binary function works — operator.mul gives you a running product, min tracks the lowest value seen so far.


Full Working Example

This example pulls together python itertools chain, product, compress, islice, groupby, combinations, and accumulate into one script that processes a small product catalogue.

import itertools

# --- Data Setup ---
categories = ["Electronics", "Clothing"]
sizes = ["S", "M", "L"]
available_flags = [1, 0, 1, 1, 0, 1]

# itertools.product: generate SKU candidates from categories x sizes
sku_candidates = list(itertools.product(categories, sizes))
print("All SKU candidates:")
for sku in sku_candidates:
 print(" ", sku)

print()

# itertools.compress: keep only in-stock SKUs
in_stock_skus = list(itertools.compress(sku_candidates, available_flags))
print("In-stock SKUs:")
for sku in in_stock_skus:
 print(" ", sku)

print()

# itertools.combinations: unique pairs of in-stock SKUs for bundle promotions
bundles = list(itertools.combinations(in_stock_skus, 2))
print(f"Possible bundle pairs: {len(bundles)}")
for bundle in itertools.islice(bundles, 4): # show first 4 only
 print(" ", bundle)

print()

# itertools.chain: merge two separate order lists into one stream
morning_orders = [("Electronics", "S"), ("Clothing", "L")]
afternoon_orders = [("Electronics", "L"), ("Clothing", "S"), ("Electronics", "M")]

all_orders = list(itertools.chain(morning_orders, afternoon_orders))
print("All orders (merged):", all_orders)

print()

# itertools.groupby: tally orders by category (must sort first)
all_orders.sort(key=lambda x: x[0])
print("Orders grouped by category:")
for category, group in itertools.groupby(all_orders, key=lambda x: x[0]):
 items = list(group)
 print(f" {category}: {len(items)} orders -> {items}")

print()

# itertools.accumulate: running order count
order_counts = [len(morning_orders), 1, 1, len(afternoon_orders) - 2]
running = list(itertools.accumulate(order_counts))
print("Running order count checkpoints:", running)
All SKU candidates:
 ('Electronics', 'S')
 ('Electronics', 'M')
 ('Electronics', 'L')
 ('Clothing', 'S')
 ('Clothing', 'M')
 ('Clothing', 'L')

In-stock SKUs:
 ('Electronics', 'S')
 ('Electronics', 'L')
 ('Clothing', 'S')
 ('Clothing', 'L')

Possible bundle pairs: 6
 (('Electronics', 'S'), ('Electronics', 'L'))
 (('Electronics', 'S'), ('Clothing', 'S'))
 (('Electronics', 'S'), ('Clothing', 'L'))
 (('Electronics', 'L'), ('Clothing', 'S'))

All orders (merged): [('Electronics', 'S'), ('Clothing', 'L'), ('Electronics', 'L'), ('Clothing', 'S'), ('Electronics', 'M')]

Orders grouped by category:
 Clothing: 2 orders -> [('Clothing', 'L'), ('Clothing', 'S')]
 Electronics: 3 orders -> [('Electronics', 'S'), ('Electronics', 'L'), ('Electronics', 'M')]

Running order count checkpoints: [2, 3, 4, 5]

Every tool in the itertools module python follows the same philosophy: return a lazy iterator, compose cleanly with other iterators, and keep your code readable. Once you start reaching for python itertools instead of manual loops, you will find yourself writing less code that does more — and that is exactly what the module was designed for.