|$ curl https://forge-ai.dev/api/markdown?path=docs/python/collections-itertools
$cat docs/collections-&-itertools.md
updated Todayยท20-26 min readยทpublished

collections & itertools

โ—†Pythonโ—†collectionsโ—†itertoolsโ—†Intermediate๐ŸŽฏFree Tools
Introduction

The collections and itertools modules are the stdlib power tools for data munging. Master them and you write less code with fewer bugs.

โ„น

info

Compose iterators lazily. Materialize with list() only when you need random access or multiple passes.
Counter
counter.py
Python
1from collections import Counter
2
3c = Counter("abracadabra")
4print(c.most_common(3))
5print(c.total()) # 3.10+
6
7c.update("aaa")
8c.subtract("ab")
9print(+c) # drop non-positive
10
11a = Counter(apples=3, oranges=1)
12b = Counter(apples=1, bananas=2)
13print(a + b, a - b, a & b, a | b)
defaultdict
defaultdict.py
Python
1from collections import defaultdict
2
3by_len: defaultdict[int, list[str]] = defaultdict(list)
4for w in ["a", "bb", "ccc", "dd"]:
5 by_len[len(w)].append(w)
6print(dict(by_len))
7
8index = defaultdict(set)
9for i, w in enumerate(["a", "b", "a"]):
10 index[w].add(i)
11
12# nested
13tree = defaultdict(lambda: defaultdict(int))
14tree["en"]["hello"] += 1
โš 

warning

defaultdict factory is called with zero args. Do not pass list.append โ€” pass list.
deque
deque.py
Python
1from collections import deque
2
3q = deque([1, 2, 3], maxlen=5)
4q.append(4)
5q.appendleft(0)
6print(q.pop(), q.popleft())
7
8# sliding window average
9def moving_avg(xs, k):
10 window = deque(maxlen=k)
11 for x in xs:
12 window.append(x)
13 if len(window) == k:
14 yield sum(window) / k
15
16print(list(moving_avg(range(10), 3)))
17
18# rotate
19d = deque("abcde")
20d.rotate(2)
21print("".join(d))
namedtuple
namedtuple.py
Python
1from collections import namedtuple
2
3User = namedtuple("User", ["id", "name", "role"], defaults=["user"])
4u = User(1, "Ada")
5print(u.id, u._asdict(), u._replace(role="admin"))
6
7# Prefer typing.NamedTuple for annotations
8from typing import NamedTuple
9
10class Point(NamedTuple):
11 x: float
12 y: float
13 def dist(self) -> float:
14 return (self.x ** 2 + self.y ** 2) ** 0.5
15
16print(Point(3, 4).dist())
๐Ÿ“

note

For mutable records with defaults, prefer dataclasses.
ChainMap
chainmap.py
Python
1from collections import ChainMap
2
3defaults = {"color": "red", "user": "guest"}
4env = {"user": "ada"}
5cli = {"color": "blue"}
6cfg = ChainMap(cli, env, defaults)
7print(cfg["color"], cfg["user"]) # blue, ada
8
9# writes hit the first map
10cfg["theme"] = "dark"
11print(cli)
itertools โ€” Core Tools
itertools_core.py
Python
1from itertools import (
2 chain, accumulate, compress, dropwhile, takewhile,
3 filterfalse, islice, pairwise, starmap, zip_longest,
4)
5
6print(list(chain([1, 2], (3, 4), "ab")))
7print(list(accumulate([1, 2, 3, 4])))
8print(list(compress("abcdef", [1, 0, 1, 0, 1, 1])))
9print(list(dropwhile(lambda x: x < 3, [1, 2, 3, 1])))
10print(list(islice(range(100), 10, 20, 2)))
11print(list(pairwise(range(4)))) # 3.10+: (0,1),(1,2),(2,3)
12print(list(zip_longest("ab", "xyz", fillvalue="-")))
13print(list(starmap(pow, [(2, 3), (3, 2)])))
groupby

Keys must be sorted first โ€” groupby only groups consecutive equal keys.

groupby.py
Python
1from itertools import groupby
2from operator import itemgetter
3
4rows = [
5 {"city": "NYC", "name": "Ada"},
6 {"city": "NYC", "name": "Grace"},
7 {"city": "SF", "name": "Alan"},
8]
9rows.sort(key=itemgetter("city"))
10for city, group in groupby(rows, key=itemgetter("city")):
11 names = [r["name"] for r in group]
12 print(city, names)
product, permutations, combinations
combo.py
Python
1from itertools import product, permutations, combinations, combinations_with_replacement
2
3print(list(product("AB", repeat=2)))
4print(list(product([0, 1], ["x", "y"])))
5print(list(permutations("ABC", 2)))
6print(list(combinations("ABCD", 2)))
7print(list(combinations_with_replacement("ABC", 2)))
8
9# Cartesian grid
10for x, y in product(range(2), range(2)):
11 print(x, y)
Infinite Iterators
infinite.py
Python
1from itertools import count, cycle, repeat, islice
2
3print(list(islice(count(10, 2), 5))) # 10,12,14,16,18
4print(list(islice(cycle("AB"), 5))) # A B A B A
5print(list(repeat("x", 3)))
more-itertools

The third-party more-itertools package adds battle-tested recipes: chunked, windowed, unique_everseen, flatten, spy, and more.

more_itertools_demo.py
Python
1# pip install more-itertools
2from more_itertools import chunked, windowed, unique_everseen, flatten
3
4print(list(chunked(range(10), 3)))
5print(list(windowed(range(5), 3)))
6print(list(unique_everseen("AABBCCAB")))
7print(list(flatten([[1, 2], [3], [4, 5]])))
Composable Recipes
recipes.py
Python
1from collections import Counter, deque
2from itertools import islice, groupby
3from collections.abc import Iterable, Iterator
4
5def batched(iterable: Iterable, n: int) -> Iterator[tuple]:
6 """Python 3.12+ has itertools.batched; this works earlier."""
7 it = iter(iterable)
8 while batch := tuple(islice(it, n)):
9 yield batch
10
11def top_n(xs: Iterable[str], n: int = 10) -> list[tuple[str, int]]:
12 return Counter(xs).most_common(n)
13
14def consume(it: Iterable, n: int | None = None) -> None:
15 if n is None:
16 deque(it, maxlen=0)
17 else:
18 next(islice(it, n, n), None)
19
20print(list(batched(range(10), 3)))
21print(top_n("abracadabra", 3))
OrderedDict (rare)

Since Python 3.7, dict preserves insertion order. OrderedDict still offers move_to_end and equality that respects order.

ordereddict.py
Python
1from collections import OrderedDict
2
3od = OrderedDict(a=1, b=2, c=3)
4od.move_to_end("a")
5print(list(od))
6od.move_to_end("b", last=False)
7print(list(od))
8print(OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1)) # False
9print(dict(a=1, b=2) == dict(b=2, a=1)) # True
When to Use What
NeedReach for
FrequenciesCounter
Multi-map / groupingdefaultdict(list/set)
Queue / stack / windowdeque
Immutable recordNamedTuple / dataclass
Layered configChainMap
Lazy pipelinesitertools + generators
Cartesian / combosproduct / combinations
Chunked batchesitertools.batched (3.12) / islice
Performance Notes
  • Counter is a dict subclass โ€” O(1) average updates
  • deque append/popleft are O(1); list.pop(0) is O(n)
  • groupby without prior sort is a common bug
  • product of large ranges can explode โ€” bound inputs
  • Prefer generator expressions over list comps when feeding itertools
perf.py
Python
1from collections import deque
2from timeit import timeit
3
4n = 10_000
5print("list pop(0)", timeit(lambda: (lambda L: [L.pop(0) for _ in range(len(L))])(list(range(n))), number=10))
6print("deque popleft", timeit(lambda: (lambda d: [d.popleft() for _ in range(len(d))])(deque(range(n))), number=10))
UserDict / UserList / UserString

Subclass-friendly wrappers when you need to customize dict/list/str behavior safely.

userdict.py
Python
1from collections import UserDict
2
3class LowerDict(UserDict):
4 def __setitem__(self, key, value):
5 super().__setitem__(str(key).lower(), value)
6 def __getitem__(self, key):
7 return super().__getitem__(str(key).lower())
8
9d = LowerDict()
10d["Host"] = "localhost"
11print(d["host"])
$Blueprint โ€” Engineering DocumentationยทSection ID: PYTHON-COLLECTIONSยทRevision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.