Python Cheatsheet
itertools and collections
Use this Python reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
itertools Reference
Every one of these returns a lazy iterator, not a list. Nothing is computed
until you iterate, and each is one-shot. Wrap in list() to see the values.
| Call | Yields | Example → |
|---|---|---|
count(start=0, step=1) | infinite counter, floats allowed | count(10, 2) → 10, 12, 14, … |
cycle(iterable) | repeats forever, caches a copy | cycle("AB") → A, B, A, B, … |
repeat(obj, times=None) | obj again and again | repeat("x", 3) → x, x, x |
chain(*iterables) | one flat stream of all args | chain("AB", "CD") → A, B, C, D |
chain.from_iterable(it) | flattens one level, lazily | [[1,2],[3]] → 1, 2, 3 |
zip_longest(*its, fillvalue=None) | pads to the longest | "AB", "xyz" → Ax, By, -z |
islice(it, stop) · (it, start, stop, step) | a slice of any iterator | islice(count(), 2, 8, 2) → 2, 4, 6 |
takewhile(pred, it) | items until pred is first false | n<3 on 1,2,3,1 → 1, 2 |
dropwhile(pred, it) | items after pred is first false | n<3 on 1,2,3,1 → 3, 1 |
filterfalse(pred, it) | the inverse of filter | bool on 0,1,0 → 0, 0 |
compress(data, selectors) | data where the selector is truthy | "ABCD", [1,0,1,0] → A, C |
starmap(fn, it) | fn(*args) per tuple | pow on (2,5),(3,2) → 32, 9 |
tee(it, n=2) | n independent iterators | see the caveat below |
pairwise(it) 3.10+ | overlapping adjacent pairs | "ABC" → AB, BC |
batched(it, n) 3.12+ | tuples of length n, last may be short | "ABCDE", 2 → AB, CD, E |
accumulate(it, fn=add, *, initial=None) | running totals | 1,2,3,4 → 1, 3, 6, 10 |
groupby(it, key=None) | (key, group) on consecutive runs | see below |
from itertools import (count, cycle, repeat, chain, zip_longest, islice, takewhile, dropwhile, filterfalse, compress, starmap, tee, pairwise, batched, accumulate, groupby) import operator list(islice(count(1), 5)) # [1, 2, 3, 4, 5] list(chain.from_iterable([[1, 2], [3], []]))# [1, 2, 3] list(zip_longest("AB", "xyz", fillvalue="-")) # [('A', 'x'), ('B', 'y'), ('-', 'z')] list(batched("ABCDE", 2)) # [('A','B'), ('C','D'), ('E',)] list(batched("ABCDE", 2, strict=True)) # 3.13+, raises if the tail is short list(accumulate([1, 2, 3, 4], operator.mul, initial=1)) # [1, 1, 2, 6, 24] list(accumulate([3, 1, 4, 1, 5], max)) # [3, 3, 4, 4, 5]
tee buffers everything the slowest branch has not consumed yet, so it is only a
win when the branches advance together. Do not touch the original iterator after
teeing it. For two full passes over a big source, a list is usually cheaper.
Combinatorics
Lengths grow fast, so guard the input size before materializing.
| Call | Count | "ABC", r=2 → |
|---|---|---|
product(a, b) | len(a) × len(b) | full cartesian pairs |
product(a, repeat=r) | n**r | AA AB AC BA BB BC CA CB CC |
permutations(a, r) | n! / (n−r)! | AB AC BA BC CA CB |
combinations(a, r) | n! / (r!(n−r)!) | AB AC BC |
combinations_with_replacement(a, r) | (n+r−1)! / (r!(n−1)!) | AA AB AC BB BC CC |
from itertools import product, permutations, combinations from itertools import combinations_with_replacement as cwr ["".join(p) for p in permutations("ABC", 2)] # ['AB','AC','BA','BC','CA','CB'] ["".join(c) for c in combinations("ABC", 2)] # ['AB', 'AC', 'BC'] ["".join(c) for c in cwr("ABC", 2)] # ['AA','AB','AC','BB','BC','CC'] list(product([0, 1], repeat=3)) # 8 binary triples list(product("ab", range(2))) # [('a',0), ('a',1), ('b',0), ('b',1)] # product replaces nested loops over a grid of settings for lr, batch in product([0.01, 0.1], [16, 32]): ...
permutations and combinations treat items by position, not value, so a
duplicate in the input produces duplicate output tuples. Order comes out
lexicographic only when the input is already sorted.
groupby, Sort First
groupby groups consecutive equal keys. Unsorted input silently produces
fragmented groups, and this is the single most common itertools bug.
from itertools import groupby from operator import itemgetter rows = [{"dept": "eng", "n": 1}, {"dept": "ops", "n": 2}, {"dept": "eng", "n": 3}] # WRONG, eng appears twice [(k, len(list(g))) for k, g in groupby(rows, key=itemgetter("dept"))] # [('eng', 1), ('ops', 1), ('eng', 1)] # RIGHT, sort by the same key first rows.sort(key=itemgetter("dept")) {k: [r["n"] for r in g] for k, g in groupby(rows, key=itemgetter("dept"))} # {'eng': [1, 3], 'ops': [2]}
The group iterator is shared and invalidated by advancing to the next group, so
list(g) inside the loop or nothing at all.
groups = list(groupby("aabbb")) # [('a', <it>), ('b', <it>)] [list(g) for _, g in groups] # [[], []] every group is empty now # Materialize as you go {k: list(g) for k, g in groupby("aabbb")} # {'a': ['a','a'], 'b': ['b','b','b']} # Run-length encode [(k, len(list(g))) for k, g in groupby("aaabbc")] # [('a',3), ('b',2), ('c',1)]
If you only need a mapping and not runs, a defaultdict(list) is one pass and
needs no sort.
Counter
A dict subclass mapping element → count. A missing key reads as 0 instead of
raising, but it is not a defaultdict (reading does not insert).
from collections import Counter c = Counter("mississippi") # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1}) c = Counter(["a", "b", "a"]) # from any iterable c = Counter({"a": 2}, b=1) # from a mapping or kwargs c["z"] # 0, no KeyError, and 'z' is NOT added c.most_common(2) # [('i', 4), ('s', 4)] ties in insertion order c.most_common()[::-1] # least common first c.total() # 11, sum of counts (3.10+) list(c.elements()) # each key repeated count times c.update("pi") # ADDS counts (dict.update would overwrite) c.subtract("pi") # subtracts, allows zero and negatives +c, -c # keep only positive · only negative, flipped c.pop("m"); del c["p"] # plain dict removal
| Op | Result | Note |
|---|---|---|
a + b | summed counts | drops non-positive |
a - b | difference | drops non-positive, unlike subtract |
a & b | per-key min | intersection |
a | b | per-key max | union |
Counter("aab") - Counter("ab") # Counter({'a': 1}) Counter("aab") & Counter("abb") # Counter({'a': 1, 'b': 1}) sum(Counter(line.split()) for line in lines) # merge word counts
defaultdict, ChainMap, OrderedDict
from collections import defaultdict, ChainMap, OrderedDict d = defaultdict(list) # factory is called with NO arguments d["a"].append(1) # {'a': [1]}, key created on read defaultdict(int)["x"] += 1 # counters defaultdict(set) # unique members per key defaultdict(lambda: defaultdict(int)) # nested, works to any depth d["missing"] # creates an empty list. Reading MUTATES the dict. d.get("missing") # None, and does not create anything dict(d) # plain dict, drops the factory
dict.setdefault is the no-import alternative, but it evaluates the default on
every call, so defaultdict wins in a loop.
plain = {}
plain.setdefault("a", []).append(1) # same effect, allocates [] each timeChainMap layers mappings for lookup without merging or copying. Writes always
land in the first map.
cfg = ChainMap(cli_args, env_vars, defaults) # first hit wins cfg["debug"] cfg.maps # the underlying list, mutable cfg.new_child({"tmp": 1}) # push a scratch layer cfg.parents # everything except the first map dict(cfg) # flatten to one plain dict
OrderedDict still has a reason to exist even though plain dicts keep insertion
order since 3.7: order-sensitive ==, move_to_end, and popitem(last=False).
OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1) # False, order matters dict(a=1, b=2) == dict(b=2, a=1) # True od.move_to_end("a") # or last=False for the front od.popitem(last=False) # FIFO pop, an LRU building block
deque
A doubly linked list with O(1) append and pop at both ends, where a list is O(n) at the front. Indexing the middle is O(n), so it is not a list replacement.
from collections import deque q = deque([1, 2, 3], maxlen=3) q.append(4) # deque([2, 3, 4]), 1 is silently DISCARDED q.appendleft(0) # pushes 4 off the right instead q.pop(), q.popleft() # both O(1) q.extend([7, 8]); q.extendleft([9]) # extendleft REVERSES its argument q.rotate(1) # right by 1: [a,b,c] → [c,a,b] q.rotate(-1) # left by 1 q.maxlen # read-only, None when unbounded
| Use | Idiom |
|---|---|
| FIFO queue | append + popleft |
| LIFO stack | a plain list is faster |
| last N lines | deque(f, maxlen=100) |
| sliding window | maxlen=n, read after each append |
| round robin | rotate(-1) each turn |
with open("app.log") as f: last10 = deque(f, maxlen=10) # constant memory over any file size
deque is thread-safe for append/popleft (they are atomic), so it works as a
lock-free producer/consumer buffer between threads. It has no sort and no slicing.
namedtuple and the User* Wrappers
A tuple subclass with named fields. Immutable, hashable, and no __dict__, so it
is as small as a tuple. For anything with methods or mutation, use a dataclass.
from collections import namedtuple Point = namedtuple("Point", "x y") # or ["x", "y"] Point = namedtuple("Point", "x y z", defaults=(0,)) # defaults fill from the RIGHT p = Point(1, 2) p.x, p[0] # attribute or index access x, y = p # unpacks like a tuple p._replace(y=99) # a NEW Point, tuples are immutable p._asdict() # {'x': 1, 'y': 2} Point._fields # ('x', 'y') Point._make([1, 2]) # build from an iterable, e.g. a csv row p == (1, 2) # True, it IS a tuple
UserDict, UserList, and UserString wrap the real data in .data so
subclassing works cleanly. Subclassing dict directly means __getitem__ is
bypassed by get, update, and the literal constructor.
from collections import UserDict class CaseInsensitive(UserDict): def __setitem__(self, key, value): super().__setitem__(key.lower(), value) def __getitem__(self, key): return super().__getitem__(key.lower()) h = CaseInsensitive() h["Content-Type"] = "text/html" h["content-type"] # 'text/html'
functools
| Tool | Purpose |
|---|---|
@cache 3.9+ | unbounded memoization, lru_cache(maxsize=None) |
@lru_cache(maxsize=128, typed=False) | bounded LRU memoization |
@cached_property | compute once per instance, stored in __dict__ |
partial(fn, *a, **kw) | freeze arguments, returns a callable |
reduce(fn, it[, initial]) | fold left to a single value |
@wraps(fn) | copy name/doc/annotations onto a wrapper |
@total_ordering | fill in the rest from __eq__ plus one of < <= > >= |
@singledispatch | overload a function on its first argument's type |
from functools import cache, lru_cache, cached_property, partial, reduce from functools import wraps, total_ordering, singledispatch import operator @cache def fib(n): return n if n < 2 else fib(n - 1) + fib(n - 2) fib(100) # instant, 354224848179261915075 fib.cache_info() # CacheInfo(hits=98, misses=101, ...) fib.cache_clear() reduce(operator.mul, [1, 2, 3, 4]) # 24 reduce(operator.or_, [{1}, {2}, {3}]) # {1, 2, 3} int2 = partial(int, base=2); int2("1011") # 11 class Report: @cached_property def rows(self): return expensive_query() # runs once per instance @singledispatch def size(x): raise TypeError(type(x)) @size.register def _(x: str): return len(x) @size.register def _(x: int): return x
Cached arguments must be hashable, and the cache keeps strong references, so
@cache on a method leaks every self it ever saw. Use @cached_property for
per-instance results instead.
operator
Function forms of the operators, faster and clearer than an equivalent lambda,
and the standard key= argument for sorting.
from operator import itemgetter, attrgetter, methodcaller rows = [{"name": "b", "age": 30}, {"name": "a", "age": 30}] sorted(rows, key=itemgetter("age", "name")) # multi-key, one pass sorted(pairs, key=itemgetter(1), reverse=True) # by tuple element max(objs, key=attrgetter("score")) sorted(objs, key=attrgetter("dept.name")) # dotted paths work list(map(methodcaller("strip"), lines)) list(map(methodcaller("split", ","), lines)) itemgetter(2, 0)("abcd") # ('c', 'a') import operator operator.add, operator.mul, operator.itruediv # arithmetic operator.eq, operator.lt, operator.contains # comparisons and `in` operator.getitem, operator.setitem, operator.delitem
Recipes
from collections import Counter, defaultdict, deque from itertools import chain, groupby, islice, pairwise, product, tee from operator import itemgetter # 1. Flatten one level list(chain.from_iterable([[1, 2], [3], [4, 5]])) # [1, 2, 3, 4, 5] # 2. Chunk into fixed sizes from itertools import batched list(batched(range(7), 3)) # 3.12+ [(0,1,2), (3,4,5), (6,)] def chunks(it, n): # any version, works on iterators it = iter(it) while part := list(islice(it, n)): yield part # 3. Dedupe, preserving first-seen order list(dict.fromkeys([3, 1, 3, 2, 1])) # [3, 1, 2] # 4. Sliding window of size n def window(it, n): it = iter(it) d = deque(islice(it, n), maxlen=n) if len(d) == n: yield tuple(d) for x in it: d.append(x); yield tuple(d) list(pairwise([1, 2, 3, 4])) # n=2 [(1,2), (2,3), (3,4)] # 5. Group by a key, one pass, no sort by_dept = defaultdict(list) for r in rows: by_dept[r["dept"]].append(r) # 6. Frequency count and the top n Counter("abracadabra").most_common(3) # [('a',5), ('b',2), ('r',2)] # 7. Cartesian grid instead of nested loops list(product(range(2), "xy")) # [(0,'x'), (0,'y'), (1,'x'), (1,'y')] # 8. Transpose rows to columns list(zip(*[(1, 2), (3, 4), (5, 6)])) # [(1, 3, 5), (2, 4, 6)] # 9. Consume an iterator without building a list deque(gen, maxlen=0)
more-itertools (python -m pip install more-itertools) ships tested versions of
chunked, windowed, unique_everseen, first, flatten, and peekable, so
reach for it before writing a fourth window helper.