Q1What's the most concise way to get the per-element occurrence count of a list nums in one line?
collections — Counter / defaultdict / deque / namedtuple
Learn Counter for tallying, defaultdict for missing keys, deque for O(1) two-ended ops and ring buffers, and namedtuple for named records through examples.
The collections module gathers specialized data structures that fill in the gaps left by list / dict / tuple. This article walks through the four you'll reach for most often in real projects: Counter / defaultdict / deque / namedtuple, in order.
What each of the four is for
Each one exists to simplify a case that's awkward to write with the plain built-ins. The table below gives you the bird's-eye view, then each section dives into how to use them.
list / dict / tuple.| Type | Problem it solves | Plain alternative |
|---|---|---|
| Counter | Counting elements one by one with a loop is verbose | for + dict tally |
| defaultdict | Need if x not in d: d[x] = ... before using a new key | dict.setdefault(...) |
| deque | list.insert(0, ...) and list.pop(0) are slow (O(n)) | list (fine for small data) |
| namedtuple | Tuple elements are only accessible by position | dataclass or dict (heavier) |
Counter — tally occurrences in one line
Counter is a class that takes an iterable (a list, string, etc.) and returns a dict of how many times each element occurs. With a plain dict, you'd write a loop that checks if the key already exists, initializes it to 1 if not, or increments it if so — but Counter does the same job in one line: Counter(list).
The return value is a subclass of dict, so you can pull values out with counter["apple"] just like a normal dict. There's also .most_common(N), which returns a list of (element, count) tuples in descending order — perfect for ranking displays.
most_common(N) returns the top N by frequency, so you can write a ranking display in one line.defaultdict — auto-initialize missing keys
defaultdict is a dict that automatically inserts a default value when you access a key that doesn't exist yet. With a plain dict, you have to write the existence check + initialization dance every time, like if key not in d: d[key] = []. With defaultdict, you pass a function that produces the initial value when you create it, and it calls that function automatically the first time a missing key is touched.
The two patterns you'll see most are defaultdict(list) (empty list for missing keys) and defaultdict(int) (0 for missing keys). The argument is a function that returns the initial value when called with no arguments — list and int both qualify (they return [] and 0 respectively), so you pass them directly.
list for an empty list, int for 0, set for an empty set.When to pick Counter vs. defaultdict
If you just want to count things, Counter is more concise (and you get most_common for free). If you're grouping into lists or sets — anything where the initial value is an empty container — defaultdict(list) / defaultdict(set) is the better fit.
deque — O(1) operations on both ends
deque (double-ended queue) is a list-like data structure that supports adding and removing from both ends. Plain list already handles right-end ops via append and pop, but list.insert(0, x) and list.pop(0) shift every element by one slot internally, so they end up O(n) (slower as the list grows).
deque is designed so both ends are O(1) (constant time regardless of length), making it the right pick for queues, history buffers, and keeping the most recent N entries — anywhere you need to push and pop from both sides. The maxlen argument is especially handy: set a max length, and adds beyond that limit silently drop the element on the opposite end, giving you a ring buffer for free.
deque(maxlen=N), you get a ring buffer that automatically drops the oldest when full.| Method | Effect | Cost |
|---|---|---|
| append(x) | Add to right end | O(1) |
| appendleft(x) | Add to left end | O(1) |
| pop() | Remove from right end | O(1) |
| popleft() | Remove from left end | O(1) |
| maxlen=N | Set max length (ring buffer) | Opposite end drops automatically |
namedtuple — lightweight records that name tuple fields
namedtuple is a function that defines a named-field tuple in one line. A plain tuple like (3, 4) is only accessible by position (p[0] / p[1]), so the reader has to remember which slot is which. namedtuple lets you write p.x / p.y with meaningful names, which is a big readability win.
It also stays compatible with regular tuples — p[0] indexing and *p unpacking still work — so you can add names to existing tuple-based code without breaking anything. It's a lightweight extension.
_asdict() converts to a dict, which plays nicely with JSON and pprint.namedtuple vs. dataclass
If you just need an immutable lightweight record, reach for namedtuple. If you want methods, default values, and detailed type hints, dataclass (covered two articles from now) is the right tool. namedtuple's strength is tuple compatibility — it's perfect for replacing existing return types like def f() -> tuple[int, int]: without breaking callers.
namedtuple extras — repr / index / _asdict
On top of class-style named access, namedtuple gives you tuple-compatible index access, a readable repr, and a _asdict method for dict conversion. You'll use these for debugging, compatibility with existing APIs, and JSON serialization, among other things.
Knowledge Check
Answer each question one by one.
Q2After creating defaultdict(list), what does the first access to a new key d["x"] give you?
Q3Which is the best fit when you want to keep only the most recent 5 items?