itertools

Reviewed & published by Brayan K

itertools is a toolbox of fast, lazy iterator helpers built right into Python. It lets you combine, slice, group, and generate sequences with clean one-liners — and crucially, without loading everything into memory at once.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Master a handful of these and your loops get shorter, faster, and far more readable.

What You'll Learn in This Lesson

1 Lazy Iterators — The Big Idea

Most itertools functions return an iterator, not a list. An iterator produces values one at a time, on demand — it doesn't build the whole sequence up front. That's what makes itertools both fast and memory-light.

import itertools

# count() is an INFINITE counter — you could never make a list of it!
counter = itertools.count(start=10, step=5)
print(next(counter))   # 10
print(next(counter))   # 15
print(next(counter))   # 20

# It only ever holds ONE value at a time. Take what you need:
first_three = list(itertools.islice(itertools.count(), 3))
print(first_three)     # [0, 1, 2]

2 Combining Sequences: chain & zip_longest

chain glues sequences end-to-end into one stream. zip_longest is like the built-in zip but doesn't stop at the shortest input:

from itertools import chain, zip_longest

# chain: flatten several iterables into one
front = [1, 2]
middle = [3, 4]
back = [5, 6]
print(list(chain(front, middle, back)))   # [1, 2, 3, 4, 5, 6]

# Great for looping over multiple lists as if they were one:
for x in chain("ab", "cd"):
    print(x, end=" ")        # a b c d
print()

# zip stops at the shortest; zip_longest fills the gaps
names = ["Ada", "Grace", "Katherine"]
scores = [95, 98]
print(list(zip_longest(names, scores, fillvalue=0)))
# [('Ada', 95), ('Grace', 98), ('Katherine', 0)]

Use chain to treat many lists as one without the cost of list1 + list2 + list3 (which copies everything).

✂️ Slicing & Filtering Streams

You can't slice an iterator with [2:5]. itertools gives you lazy equivalents:

from itertools import islice, takewhile, dropwhile

nums = [1, 3, 5, 8, 9, 2, 11]

# islice: slice an iterator (start, stop[, step])
print(list(islice(nums, 2, 5)))     # [5, 8, 9]

# takewhile: take items WHILE the test is true, then stop
print(list(takewhile(lambda n: n < 8, nums)))   # [1, 3, 5]

# dropwhile: SKIP items while the test is true, then keep the rest
print(list(dropwhile(lambda n: n < 8, nums)))   # [8, 9, 2, 11]

3 Running Totals with accumulate

accumulate produces a running result — a cumulative sum by default, or any function you supply. Perfect for running balances and high-water marks:

from itertools import accumulate
import operator

sales = [100, 50, 200, 75]

# Running total (cumulative sum)
print(list(accumulate(sales)))                  # [100, 150, 350, 425]

# Running maximum
print(list(accumulate(sales, max)))             # [100, 100, 200, 200]

# Running product
print(list(accumulate([1, 2, 3, 4], operator.mul)))  # [1, 2, 6, 24]

The last element of a plain accumulate is the same as sum() — but you also get every intermediate step, which is exactly what charts and dashboards need.

from itertools import accumulate, chain, islice

# Daily steps for two weeks, stored as two lists
week1 = [4200, 5100, 3000, 8000, 6500, 9000, 2000]
week2 = [7000, 6800, 4500, 5200, 8800, 3300, 9500]

# chain them into one continuous stream
all_days = list(chain(week1, week2))
print("Total days:", len(all_days))           # 14
print("Grand total:", sum(all_days), "steps")

# Running total across the whole fortnight
cumulative = list(accumulate(all_days))
print("After day 7:", cumulative[6], "steps")
print("After day 14:", cumulative[-1], "steps")

# The 3 highest-step days (sort a copy, take first 3)
top3 = list(islice(sorted(all_days, reverse=True), 3))
print("Top 3 days:", top3)

# ✅ Expected output:
# Total days: 14
# Grand total: 82900 steps
# After day 7: 37800 steps
# After day 14: 82900 steps
# Top 3 days: [9500, 9000, 8800]

4 Grouping with groupby (the gotcha)

groupby groups consecutive equal items. This is the #1 itertools surprise — you almost always sort first:

from itertools import groupby

people = [
    {"name": "Ada", "team": "A"},
    {"name": "Leo", "team": "B"},
    {"name": "Maya", "team": "A"},
    {"name": "Zara", "team": "B"},
]

# WRONG: not sorted, so each item ends up in its own group
key = lambda p: p["team"]
for team, members in groupby(people, key=key):
    print(team, [m["name"] for m in members])
# A ['Ada']   B ['Leo']   A ['Maya']   B ['Zara']   <- broken!

print("---")

# RIGHT: sort by the same key first
people.sort(key=key)
for team, members in groupby(people, key=key):
    print(team, [m["name"] for m in members])
# A ['Ada', 'Maya']
# B ['Leo', 'Zara']

🎲 Combinatorics: product, permutations, combinations

These replace deeply nested loops with a single readable call:

from itertools import product, permutations, combinations

# product: every combination across iterables (nested loops, flattened)
sizes = ["S", "M", "L"]
colors = ["red", "blue"]
for s, c in product(sizes, colors):
    print(s, c)          # S red, S blue, M red, M blue, L red, L blue

# permutations: ORDER MATTERS  -> (A,B) and (B,A) both appear
print(list(permutations("ABC", 2)))
# [('A','B'),('A','C'),('B','A'),('B','C'),('C','A'),('C','B')]  -> 6

# combinations: ORDER DOESN'T  -> only (A,B), never (B,A)
print(list(combinations("ABC", 2)))
# [('A','B'),('A','C'),('B','C')]  -> 3

Reach for product whenever you'd otherwise write nested for loops. Use combinations for "pick a group" and permutations for "arrange in order".

🧪 Worked Example — Four itertools Tools on One Job

Each section above showed one function on its own. Here they work together on a task you would genuinely be handed: two lists of orders arriving from different systems, and a summary to produce. chain merges them, sort + groupby aggregates them, accumulate gives the running total, and islice takes a slice of an endless counter.

from itertools import chain, groupby, accumulate, islice, count

# Two batches of orders that arrived from different systems.
morning = [{"item": "tea", "qty": 2}, {"item": "cake", "qty": 1}]
evening = [{"item": "tea", "qty": 3}, {"item": "coffee", "qty": 4}, {"item": "cake", "qty": 2}]

# ---- chain: treat both lists as one stream ---------------------------------
# Unlike morning + evening, chain does not build a merged copy as it goes —
# it walks the first list, then the second.
orders = list(chain(morning, evening))
print(len(orders))

# ---- sort + groupby: the aggregation pattern -------------------------------
# groupby only groups CONSECUTIVE equal keys, so sorting by the SAME key first
# is not optional. Skip the sort and "tea" comes back as two separate groups.
by_item = lambda o: o["item"]
orders.sort(key=by_item)

for item, group in groupby(orders, key=by_item):
    rows = list(group)                   # each group is a one-shot iterator,
                                         # so materialise it before using it twice
    print(item, sum(r["qty"] for r in rows))

# ---- accumulate: every intermediate step, not just the final answer --------
quantities = [o["qty"] for o in orders]  # in the sorted order from above
print(quantities)
print(list(accumulate(quantities)))      # the running total after each order

# ---- islice + count: take a slice of something endless ---------------------
# count() never stops, so list(count()) would hang forever. islice is how you
# safely take a fixed number of values off an infinite iterator.
ticket_numbers = islice(count(start=1000, step=1), 4)
print(list(ticket_numbers))

# ✅ Expected output:
# 5
# cake 3
# coffee 4
# tea 5
# [1, 2, 4, 2, 3]
# [1, 3, 7, 9, 12]
# [1000, 1001, 1002, 1003]

Notice that quantities prints as [1, 2, 4, 2, 3], not in the original order. The orders.sort(...) line rearranged the list in place — sorting for groupby changes your data, so sort a copy if the original order still matters to you later.

🎯 Your Turn — Name That Tool

Four blanks, four itertools names you have already met. Everything else is written for you — run it and check against the expected output.

from itertools import chain, islice, accumulate, groupby

# 🎯 YOUR TURN — fill in the four blanks marked ___

first_half = [10, 20, 30]
second_half = [40, 50]

# 👉 blank 1: the function that glues both lists into one stream
merged = list(___(first_half, second_half))
print(merged)

# 👉 blank 2: the function that turns values into a RUNNING total
print(list(___(merged)))

# 👉 blank 3: the function that slices an iterator — here, the first 3 values
print(list(___(merged, 3)))

words = ["hi", "to", "tea", "sun", "a"]
words.sort(key=len)          # sorted first, because groupby needs runs

# 👉 blank 4: the keyword argument that tells groupby what to group BY
for size, group in groupby(words, ___=len):
    print(size, list(group))

# ✅ Expected output:
# [10, 20, 30, 40, 50]
# [10, 30, 60, 100, 150]
# [10, 20, 30]
# 1 ['a']
# 2 ['hi', 'to']
# 3 ['tea', 'sun']

If every word ends up in its own group, the words.sort(key=len) line has gone missing — that is the same trap the groupby section warned about, in miniature.

Real-World Example: Summarising Sales by Region

A common reporting task: take a flat list of transactions and produce per-region totals. sort + groupby handles it elegantly:

from itertools import groupby

sales = [
    {"region": "North", "amount": 120},
    {"region": "South", "amount": 80},
    {"region": "North", "amount": 200},
    {"region": "East",  "amount": 50},
    {"region": "South", "amount": 175},
    {"region": "North", "amount": 90},
]

by_region = lambda s: s["region"]
sales.sort(key=by_region)            # MUST sort before groupby

print(f'{"Region":8}{"Orders":>8}{"Total":>10}')
print("-" * 26)
for region, group in groupby(sales, key=by_region):
    rows = list(group)               # materialise the one-shot iterator
    total = sum(r["amount"] for r in rows)
    print(f'{region:8}{len(rows):>8}{total:>10}')

# Region   Orders     Total
# --------------------------
# East          1         50
# North         3        410
# South         2        255

This same shape — sort, group, aggregate — is the backbone of countless reports. Once it clicks, you'll reach for it constantly.

from itertools import combinations

# A pizza shop offers these toppings. How many 2-topping pizzas
# can a customer build, and what are they?
toppings = ["mushroom", "pepperoni", "olive", "onion"]

pairs = list(combinations(toppings, 2))
print("Number of 2-topping pizzas:", len(pairs))   # 6

for a, b in pairs:
    print(f"- {a} + {b}")

# Order doesn't matter for a pizza, so we use combinations, not
# permutations. (mushroom+olive is the same pizza as olive+mushroom.)

# ✅ Expected output:
# Number of 2-topping pizzas: 6
# - mushroom + pepperoni
# - mushroom + olive
# - mushroom + onion
# - pepperoni + olive
# - pepperoni + onion
# - olive + onion

🎯 Mini-Challenge: The Menu Builder

One brief, one outline, no logic given. It hinges on choosing correctly between product (every pairing across two different lists) and combinations (every pair drawn from one list, order ignored).

# 🎯 MINI-CHALLENGE: a menu builder
#
# Your data:
#   starters = ["soup", "salad"]
#   mains = ["fish", "pasta", "steak"]
#
# 1. Build every (starter, main) pairing and print each one as:   soup + fish
# 2. Print the count in the form:   6 pairings
# 3. Now list every pair of two DIFFERENT mains a couple could share,
#    printed as:   fish & pasta
#    (fish & pasta and pasta & fish are the same order, so it must appear once)
# 4. Print that count in the form:   3 shared plates
#
# ✅ Expected output:
# soup + fish
# soup + pasta
# soup + steak
# salad + fish
# salad + pasta
# salad + steak
# 6 pairings
# fish & pasta
# fish & steak
# pasta & steak
# 3 shared plates

# your code here

Getting 6 shared plates instead of 3 means you reached for permutations — it counts fish, pasta and pasta, fish as two different answers.

🧩 Reorder Challenge

These lines should print groups of consecutive equal letters, but they're scrambled. Find the order:

A)     print(letter, len(list(group)))
B) from itertools import groupby
C) data = "aaabbbcca"
D) for letter, group in groupby(data):

Correct order: B → C → D → A

from itertools import groupby
data = "aaabbbcca"
for letter, group in groupby(data):
    print(letter, len(list(group)))
# a 3
# b 3
# c 2
# a 1   <- the trailing 'a' is a NEW group (not consecutive with the first)

This is the perfect groupby use case: the data is already a run of consecutive characters, so no sort is needed. Notice the final 'a' forms its own group.

🧠 Quick Recall — Predict the Output

from itertools import chain
print(list(chain([1, 2], [3], [4, 5])))

[1, 2, 3, 4, 5] — chain concatenates the iterables end-to-end into one flat sequence.

from itertools import accumulate
print(list(accumulate([3, 1, 4, 1])))

[3, 4, 8, 9] — a running sum: 3, 3+1, 3+1+4, 3+1+4+1.

from itertools import combinations
print(len(list(combinations("WXYZ", 2))))

6 — choosing 2 from 4 where order doesn't matter gives 6 pairs: WX, WY, WZ, XY, XZ, YZ.

❓ Frequently Asked Questions

Your loops just leveled up!

You can chain and zip sequences, slice and filter streams lazily, build running totals with accumulate, group data correctly, and enumerate combinations and permutations. These tools make your code shorter, faster, and clearer.

Practice quiz

What does list(chain([1, 2], [3], [4, 5])) return?

  • [[1, 2], [3], [4, 5]]
  • [(1, 3), (2, 4)]
  • [1, 2, 3, 4, 5]
  • [1, 3, 4]

Answer: [1, 2, 3, 4, 5]. chain concatenates the iterables end-to-end into one flat sequence.

Why do most itertools functions return iterators instead of lists?

  • They produce values lazily, one at a time, using little memory
  • To sort the data automatically
  • To make the code shorter only
  • Because lists are deprecated

Answer: They produce values lazily, one at a time, using little memory. Lazy iterators yield values on demand, so you can work with huge or infinite sequences cheaply.

What does list(accumulate([3, 1, 4, 1])) return?

  • [3, 1, 4, 1]
  • [9]
  • [1, 1, 3, 4]
  • [3, 4, 8, 9]

Answer: [3, 4, 8, 9]. accumulate gives a running sum: 3, 3+1, 3+1+4, 3+1+4+1.

What does list(islice([1, 3, 5, 8, 9, 2, 11], 2, 5)) return?

  • [1, 3, 5]
  • [5, 8, 9]
  • [8, 9, 2]
  • [3, 5, 8]

Answer: [5, 8, 9]. islice(iterable, start, stop) takes indices 2, 3, 4 — the values 5, 8, 9.

What is the key requirement before using groupby?

  • The data must be sorted by the same key, since it only groups consecutive equal items
  • The data must be a dict
  • The data must be numeric
  • The data must be reversed

Answer: The data must be sorted by the same key, since it only groups consecutive equal items. groupby only groups CONSECUTIVE equal elements, so you almost always sort by the key first.

How many pairs does list(combinations("WXYZ", 2)) produce?

  • 4
  • 12
  • 6
  • 16

Answer: 6. Choosing 2 of 4 where order does not matter gives 6 pairs: WX, WY, WZ, XY, XZ, YZ.

When should you use permutations instead of combinations?

  • When you want fewer results
  • When order matters, so (A,B) and (B,A) are distinct
  • When the data is sorted
  • When you need only unique values

Answer: When order matters, so (A,B) and (B,A) are distinct. permutations treats order as significant, producing more results than combinations.

What does takewhile(lambda n: n < 8, [1, 3, 5, 8, 9, 2, 11]) yield?

  • [1, 3, 5, 2]
  • [8, 9, 2, 11]
  • [1, 3, 5, 8]
  • [1, 3, 5]

Answer: [1, 3, 5]. takewhile takes items while the test is true and STOPS at the first failure (8), giving [1, 3, 5].

How does zip_longest differ from the built-in zip?

  • It sorts the inputs
  • It does not stop at the shortest input and fills gaps with fillvalue
  • It stops at the shortest input
  • It only works on strings

Answer: It does not stop at the shortest input and fills gaps with fillvalue. zip_longest keeps going to the longest input, filling missing positions with fillvalue.

What does product(["S", "M", "L"], ["red", "blue"]) replace?

  • A single sort call
  • A groupby call
  • Nested for loops over the iterables
  • A list comprehension filter

Answer: Nested for loops over the iterables. product yields every combination across the iterables — the flattened result of nested loops (6 pairs here).

Continue this course

Frequently asked questions

What is itertools?

itertools is a built-in Python module of fast, memory-efficient building blocks for working with iterators. It provides tools for combining, filtering, grouping and generating sequences without building giant lists in memory.

Why do itertools functions return iterators instead of lists?

Iterators are lazy — they produce values one at a time, only when asked. This means you can work with huge or even infinite sequences using almost no memory. Wrap the result in list() if you actually need all the values at once.

What is the difference between chain and zip?

chain joins sequences end-to-end into one long sequence ([1,2] then [3,4] -> 1,2,3,4). zip pairs them up element by element ([1,2] with [3,4] -> (1,3),(2,4)). chain concatenates; zip interleaves.

What does groupby require to work correctly?

itertools.groupby only groups CONSECUTIVE equal elements. You almost always need to sort the data by the same key first, otherwise identical items in different positions end up in separate groups.

When should I use combinations vs permutations?

Use combinations when order does not matter (choosing a team of 2 from 4 people). Use permutations when order does matter (arranging people in 1st/2nd place). permutations produces more results because (A,B) and (B,A) are distinct.