itertools — The Standard Library for Iterators

Python’s itertools module is the standard library’s toolbox for working with iterators and infinite sequences. It provides fast, memory-efficient building blocks that replace verbose loops and comprehensions with expressive, single-function calls. The functions in itertools are implemented in C and are faster than equivalent Python code. Knowing the right itertools function turns a five-line loop into a clear one-liner. For FastAPI development, islice is used for pagination, groupby for grouping database results, chain for combining query results, and product and combinations for generating test data.

Infinite Iterators

from itertools import count, cycle, repeat

# count(start, step) — infinite counter
from itertools import count, islice
counter = count(1)          # 1, 2, 3, 4, ... forever
first_5 = list(islice(counter, 5))   # [1, 2, 3, 4, 5]

counter10 = count(0, 10)    # 0, 10, 20, 30, ...
list(islice(counter10, 4))  # [0, 10, 20, 30]

# cycle(iterable) — repeat iterable forever
colours = cycle(["red", "green", "blue"])
first_7 = list(islice(colours, 7))
# ["red", "green", "blue", "red", "green", "blue", "red"]

# repeat(object, times) — repeat a value N times (or forever)
list(repeat("x", 4))   # ["x", "x", "x", "x"]

# Practical: assign round-robin IDs
server_ids = cycle(["server-1", "server-2", "server-3"])
tasks = ["task-a", "task-b", "task-c", "task-d", "task-e"]
assignments = list(zip(tasks, server_ids))
# [("task-a", "server-1"), ("task-b", "server-2"), ...]
Note: itertools functions are lazy — they return iterator objects, not lists. Always wrap with list() if you need to materialise the result or pass it to a function that requires a list. Most itertools functions also accept any iterable — not just lists — making them composable with other generators and itertools functions in lazy pipelines.
Tip: itertools.groupby() requires the input to be sorted by the grouping key before calling it. Unlike SQL’s GROUP BY which works on unsorted data, Python’s groupby creates a new group whenever the key changes. If the data is not sorted, items with the same key will end up in multiple groups. Always sort with the same key before groupby: sorted(data, key=key_func) then groupby(sorted_data, key=key_func).
Warning: itertools.tee(iterable, n) creates n independent iterators from a single iterable. However, it buffers all values that have been read by one iterator but not yet by the others — if the iterators advance at very different rates, memory usage can grow unboundedly. Prefer converting to a list for random access, and use tee only when both iterators will be advanced roughly in parallel.

Slicing and Combining

from itertools import islice, chain, chain_from_iterable, zip_longest

# islice(iterable, stop) or islice(iterable, start, stop, step)
data = range(100)
page_1 = list(islice(data, 0, 10))    # [0..9]  — first 10
page_2 = list(islice(data, 10, 20))   # [10..19] — second 10
first_5 = list(islice(data, 5))       # [0..4]

# FastAPI pagination with islice (for in-memory data)
def paginate_generator(gen, page: int, size: int):
    start = (page - 1) * size
    return list(islice(gen, start, start + size))

# chain(*iterables) — flatten one level of nesting
from itertools import chain
a = [1, 2, 3]
b = [4, 5, 6]
c = [7, 8, 9]
combined = list(chain(a, b, c))   # [1,2,3,4,5,6,7,8,9]

# Combine multiple query results
recent_posts    = db.query(Post).order_by(Post.created_at.desc()).limit(5).all()
featured_posts  = db.query(Post).filter(Post.featured).limit(3).all()
all_posts = list(chain(featured_posts, recent_posts))   # featured first

# chain.from_iterable — flatten one level from a single nested iterable
nested = [[1, 2], [3, 4], [5, 6]]
flat   = list(chain.from_iterable(nested))   # [1,2,3,4,5,6]

# zip_longest — like zip but fills shorter with fillvalue
names  = ["Alice", "Bob"]
scores = [95, 87, 72]
from itertools import zip_longest
pairs = list(zip_longest(names, scores, fillvalue="N/A"))
# [("Alice", 95), ("Bob", 87), ("N/A", 72)]

Grouping and Filtering

from itertools import groupby, takewhile, dropwhile, filterfalse

# groupby — group consecutive items with the same key
# MUST be sorted by the same key first!
posts = [
    {"author": "Alice", "title": "A1"},
    {"author": "Alice", "title": "A2"},
    {"author": "Bob",   "title": "B1"},
    {"author": "Alice", "title": "A3"},   # not consecutive with other Alice posts!
]

# Sort first
posts_sorted = sorted(posts, key=lambda p: p["author"])

# Group by author
for author, group in groupby(posts_sorted, key=lambda p: p["author"]):
    author_posts = list(group)
    print(f"{author}: {len(author_posts)} posts")
# Alice: 3 posts
# Bob: 1 post

# takewhile — take items while condition is True, stop at first False
data = [2, 4, 6, 7, 8, 10]
evens = list(takewhile(lambda x: x % 2 == 0, data))   # [2, 4, 6] — stops at 7

# dropwhile — skip items while condition is True, then yield rest
odds_and_rest = list(dropwhile(lambda x: x % 2 == 0, data))   # [7, 8, 10]

# filterfalse — opposite of filter (items where condition is False)
from itertools import filterfalse
non_admins = list(filterfalse(lambda u: u.role == "admin", users))

Combinatorics

from itertools import product, combinations, permutations, combinations_with_replacement

# product — Cartesian product (all combinations across multiple iterables)
colours = ["red", "blue"]
sizes   = ["S", "M", "L"]
items   = list(product(colours, sizes))
# [("red","S"),("red","M"),("red","L"),("blue","S"),("blue","M"),("blue","L")]

# product with repeat — e.g. all 2-char lowercase strings
import string
two_chars = list(product(string.ascii_lowercase, repeat=2))
# [("a","a"), ("a","b"), ..., ("z","z")] — 676 combinations

# combinations — unique pairs (no repetition, order doesn't matter)
letters = ["a", "b", "c", "d"]
pairs   = list(combinations(letters, 2))
# [("a","b"), ("a","c"), ("a","d"), ("b","c"), ("b","d"), ("c","d")]

# permutations — all ordered arrangements
list(permutations(["a", "b", "c"], 2))
# [("a","b"), ("a","c"), ("b","a"), ("b","c"), ("c","a"), ("c","b")]

# Practical: generate test data combinations
roles   = ["user", "editor", "admin"]
actions = ["read", "write", "delete"]
test_cases = list(product(roles, actions))   # 9 combinations for permission tests

Common Mistakes

Mistake 1 — Using groupby without sorting first

❌ Wrong — unsorted data creates multiple groups for the same key:

data = [{"k": "a"}, {"k": "b"}, {"k": "a"}]   # "a" appears non-consecutively
for key, group in groupby(data, key=lambda x: x["k"]):
    print(key, list(group))
# a [{"k": "a"}]
# b [{"k": "b"}]
# a [{"k": "a"}]   ← TWO "a" groups! Should be one

✅ Correct — sort first:

for key, group in groupby(sorted(data, key=lambda x: x["k"]), key=lambda x: x["k"]):
    print(key, list(group))
# a [{"k": "a"}, {"k": "a"}]   ✓ one group
# b [{"k": "b"}]

Mistake 2 — Consuming the group iterator from groupby after advancing

❌ Wrong — group iterator is consumed by advancing the outer iterator:

for key, group in groupby(data, key=...):
    pass   # advance outer iterator — group iterator is now exhausted!

# Can't iterate group after this — it's consumed

✅ Correct — materialise the group immediately:

groups = {key: list(group) for key, group in groupby(sorted_data, key=...)}   # ✓

Mistake 3 — Calling list() on an islice without bound — infinite loop

❌ Wrong — count() is infinite:

list(count())   # hangs forever — infinite iterator!

✅ Correct — always bound infinite itertools:

list(islice(count(), 10))   # [0,1,2,...,9] ✓

Quick Reference — Most Useful itertools

Function What It Does FastAPI Use
islice(it, n) Take first n items Pagination, early stop
chain(a, b) Combine iterables Merge query result sets
chain.from_iterable Flatten nested iterable Flatten tag lists
groupby(it, key) Group consecutive items Group DB rows by FK
product(a, b) Cartesian product Test data generation
combinations(it, r) Unique r-length pairs Pair testing
cycle(it) Repeat iterable forever Round-robin assignment
count(start) Infinite counter Auto-incrementing IDs
takewhile(pred, it) Take while predicate True Read until sentinel
filterfalse(pred, it) Exclude where predicate True Remove matching items

🧠 Test Yourself

You call groupby(posts, key=lambda p: p["author"]) and get multiple groups for author “Alice” instead of one. What is the cause?