Quick Answer

collections gives you Counter for frequencies, defaultdict for grouping, deque for fast operations at both ends, and namedtuple for lightweight records. itertools gives you combinatorics and iterator plumbing.

Counter: frequency counting, solved

from collections import Counter

words = "the quick the lazy the dog".split()
print(Counter(words).most_common(2))
# [('the', 3), ('quick', 1)]

This replaces the loop everyone writes with dict.get(w, 0) + 1. It is shorter, and most_common(n) handles the sorting that would otherwise be a second step.

Counter also supports arithmetic, which is occasionally exactly what you need:

a = Counter("listen"); b = Counter("silent")
print(a == b)        # True -- anagram check
print(a - Counter("lis"))   # what remains

The equality comparison is the neatest anagram solution there is, and it is O(n) against sorting's O(n log n).

defaultdict: grouping without the guard clause

from collections import defaultdict

dd = defaultdict(list)
dd["a"].append(1)
dd["a"].append(2)
print(dict(dd))   # {'a': [1, 2]}

With a plain dict, the first append raises KeyError because the list does not exist yet, so you write if key not in d: d[key] = [] every time. defaultdict creates it on first access using the factory you supplied.

Grouping records is where it shines:

by_stream = defaultdict(list)
for s in students:
    by_stream[s.stream].append(s.name)

One caution: reading a missing key also creates it, so if dd["nope"] silently adds an empty list. Use .get() or in when you only want to check.

deque and namedtuple

from collections import deque

dq = deque([1, 2, 3])
dq.appendleft(0)
dq.append(4)
print(list(dq))     # [0, 1, 2, 3, 4]
print(dq.popleft()) # 0

A list's pop(0) is O(n) because every remaining element shifts down. deque.popleft() is O(1). That matters in breadth-first search and any queue processing, where pop(0) in a loop turns a linear algorithm quadratic.

This is the practical answer to "should I implement a linked list" in Python — you almost never should, because deque already gives you fast operations at both ends.

from collections import namedtuple

Point = namedtuple("Point", "x y")
pt = Point(3, 4)
print(pt, pt.x)   # Point(x=3, y=4) 3

namedtuple gives readable field access with tuple behaviour and very little memory. For anything needing mutability or defaults, a dataclass is the better choice.

itertools: the ones actually worth knowing

import itertools

print(list(itertools.chain([1,2], [3,4])))
# [1, 2, 3, 4]

print(list(itertools.combinations("abc", 2)))
# [('a','b'), ('a','c'), ('b','c')]

chain iterates several sequences as one without building a combined list. combinations and permutations save writing nested loops for subset problems, and appear constantly in brute-force interview solutions.

Others worth remembering: product for the cartesian product (nested loops flattened), islice to take the first n of an iterator without materialising it, zip_longest when sequences differ in length, and accumulate for running totals.

Most return lazy iterators, so nothing is computed until consumed and they can be exhausted only once. That is the point — chain over two enormous files uses almost no memory — but it surprises people who print one and see <itertools.chain object>.

The groupby trap

itertools.groupby does not behave like SQL's GROUP BY, and this catches almost everyone once.

print([(k, list(g)) for k, g in itertools.groupby(sorted("aabbc"))])
# [('a', ['a','a']), ('b', ['b','b']), ('c', ['c'])]

That works because the input was sorted. groupby only groups consecutive equal items. On unsorted input it produces a new group every time the value changes, so the same key appears several times and the result looks wrong without any error.

So either sort by the same key first, or use defaultdict(list), which does not care about order and is usually clearer for genuine grouping.

The second gotcha: the group is an iterator sharing state with the outer one, so it is invalidated as soon as you advance. If you need to keep it, wrap it in list() immediately, as above.

Frequently Asked Questions

When should I use defaultdict instead of a normal dict? When you are grouping or accumulating and would otherwise write a check for a missing key on every insert. Be aware that reading a missing key also creates it.
Why is deque faster than a list for queues? Removing from the front of a list shifts every remaining element, which is O(n). deque is built for constant-time operations at both ends, so it stays O(1).
Why does itertools.groupby give strange results? It groups only consecutive equal items. Sort by the same key first, or use defaultdict(list), which groups regardless of order.
Why does printing an itertools function show an object? Most return lazy iterators that compute values on demand. Wrap in list() to materialise them — and remember an iterator can be consumed only once.
namedtuple or dataclass? namedtuple for lightweight immutable records that should behave like tuples. dataclass when you want mutability, defaults, methods or clearer inheritance.