Python Tutorial
The itertools and functools Modules
itertools and functools are two standard-library modules that make functional-style Python concise and fast. itertools provides building blocks for efficient looping — infinite counters, chaining, grouping, slicing iterators, combinations and permutations — all lazily evaluated. functools provides tools for working with functions — caching, partial application, reduction, ordering and decorators.
This lesson walks through the most useful functions of both modules with practical examples.
Infinite and Terminating Iterators
count(start, step), cycle(iterable) and repeat(x, n) generate values endlessly (combine with islice or zip). chain() joins iterables, islice() slices any iterator, accumulate() produces running totals, takewhile()/dropwhile() cut sequences by a condition, compress() filters by a selector, pairwise() yields overlapping pairs, batched() (3.12+) splits into fixed-size chunks, and zip_longest() pads uneven iterables.
Grouping and Combinatorics
groupby(iterable, key) groups consecutive items with the same key, so sort by the key first. product() gives the Cartesian product (nested loops), permutations() ordered arrangements, combinations() unordered selections, and combinations_with_replacement().
functools
lru_cache and cache memoise function results; partial() pre-fills some arguments of a function; reduce() folds a sequence into one value; wraps() preserves metadata in decorators; total_ordering completes comparison methods; cached_property caches a computed attribute; singledispatch dispatches on type; cmp_to_key adapts old-style comparison functions for sorting.
Examples
Infinite iterators and iterator tools
from itertools import count, cycle, repeat, islice, chain, accumulate, takewhile, dropwhile, compress, pairwise, batched, zip_longest
print(list(islice(count(100, 5), 4)))
print(list(zip(["Mon", "Tue", "Wed", "Thu"], cycle(["on-call", "off"]))))
print(list(repeat("ab", 3)))
print(list(chain([1, 2], (3,), "ab")))
print(list(accumulate([100, 250, -50, 400])))
print(list(takewhile(lambda x: x < 5, [1, 3, 6, 2])), list(dropwhile(lambda x: x < 5, [1, 3, 6, 2])))
print(list(compress("ABCDEF", [1, 0, 1, 0, 1, 1])))
print(list(pairwise([10, 13, 11, 20])))
print(list(batched(range(1, 8), 3)))
print(list(zip_longest("abc", [1, 2], fillvalue="-")))
[100, 105, 110, 115]
[('Mon', 'on-call'), ('Tue', 'off'), ('Wed', 'on-call'), ('Thu', 'off')]
['ab', 'ab', 'ab']
[1, 2, 3, 'a', 'b']
[100, 350, 300, 700]
[1, 3] [6, 2]
['A', 'C', 'E', 'F']
[(10, 13), (13, 11), (11, 20)]
[(1, 2, 3), (4, 5, 6), (7,)]
[('a', 1), ('b', 2), ('c', '-')]
groupby and combinatorics
from itertools import groupby, product, permutations, combinations, combinations_with_replacement
sales = [("north", 100), ("south", 50), ("north", 70), ("south", 30), ("east", 90)]
sales.sort(key=lambda s: s[0]) # groupby needs sorted input
for region, rows in groupby(sales, key=lambda s: s[0]):
print(region, sum(amount for _, amount in rows))
print(list(product("AB", [1, 2])))
print(["".join(p) for p in permutations("abc", 2)])
print(list(combinations([1, 2, 3, 4], 2)))
print(len(list(combinations_with_replacement("xyz", 2))))
east 90
north 170
south 80
[('A', 1), ('A', 2), ('B', 1), ('B', 2)]
['ab', 'ac', 'ba', 'bc', 'ca', 'cb']
[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
6
functools: partial, reduce, lru_cache, cmp_to_key and wraps
from functools import partial, reduce, lru_cache, cmp_to_key, wraps
import operator
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(7), cube(2))
print(reduce(operator.mul, range(1, 6)), reduce(lambda a, b: a + b, ["a", "b", "c"]))
@lru_cache(maxsize=128)
def slow_price(item):
print(f"looking up {item}...")
return len(item) * 100
print(slow_price("pen"), slow_price("pen"), slow_price.cache_info().hits)
def by_length_then_alpha(a, b):
return (len(a) - len(b)) or ((a > b) - (a < b))
print(sorted(["kiwi", "fig", "apple", "date"], key=cmp_to_key(by_length_then_alpha)))
def shout(func):
@wraps(func)
def wrapper(*args):
return func(*args).upper()
return wrapper
@shout
def greet(name):
"""Return a greeting."""
return f"hello {name}"
print(greet("asha"), greet.__name__, greet.__doc__)
49 8
120 abc
looking up pen...
300 300 1
['fig', 'date', 'kiwi', 'apple']
HELLO ASHA greet Return a greeting.
Common Mistakes
- Using groupby on unsorted data and getting several groups for the same key.
- Materialising huge combinatoric iterators with list() — they grow factorially.
- Caching functions with lru_cache whose arguments are unhashable (lists) or whose results change over time.
- Forgetting functools.wraps in decorators, losing the original name and docstring.
Key Points to Remember
- itertools gives lazy building blocks: count, cycle, chain, islice, accumulate, pairwise, batched...
- groupby groups consecutive items — sort by the key first.
- product, permutations and combinations generate combinatorial sequences.
- functools: partial, reduce, lru_cache/cache, wraps, total_ordering, cmp_to_key.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.