Learn by reading through in order

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.

The four collections types
CounterTally occurrencesdefaultdictAuto-init missing keysdequeO(1) on both endsnamedtupleName the tuple fields
Counter is for tallying, defaultdict handles missing keys, deque gives fast two-ended operations, and namedtuple adds names to tuples. All four are extension types that layer convenience on top of plain list / dict / tuple.
TypeProblem it solvesPlain alternative
CounterCounting elements one by one with a loop is verbosefor + dict tally
defaultdictNeed if x not in d: d[x] = ... before using a new keydict.setdefault(...)
dequelist.insert(0, ...) and list.pop(0) are slow (O(n))list (fine for small data)
namedtupleTuple elements are only accessible by positiondataclass 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.

How Counter works
List[apple, banana, apple, ...]Counter(list)Counter{apple: 3, banana: 2, ...}tally
Just pass an iterable and you get back a dict subclass with the occurrence count per element. most_common(N) returns the top N by frequency, so you can write a ranking display in one line.

Tally six purchase records by product using Counter. Feel how a single line replaces a regular for loop.

① Import Counter from collections

② Define the purchase list ["apple", "banana", "apple", "cherry", "banana", "apple"]

③ Pass the list to Counter to build the tally and print it as Count: ◯

④ Print the count of apple as apple count: ◯ (you can use the same square-bracket access as dict)

⑤ Use most_common to grab the top 2 and print them as Top 2: ◯

(If your code runs correctly, the explanation will appear.)

Python Editor

Run code to see output

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 argumentslist and int both qualify (they return [] and 0 respectively), so you pass them directly.

dict vs. defaultdict
Plain dictd["x"] (no key)→ KeyErrorNeed:if x not in d: d[x] = []defaultdict(list)d["x"] (no key)→ Auto-creates []Just call .append(...)directly
Plain dict raises KeyError on missing keys. defaultdict calls the function you passed at construction time and uses the result. Pass 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 containerdefaultdict(list) / defaultdict(set) is the better fit.

Group fruits by color. With defaultdict(list), the first time you touch a color key, it auto-creates an empty list, so you can call .append without an existence check.

① Import defaultdict from collections

② Create a defaultdict whose default value is an empty list

③ Register these three entries in order:

- "red""apple"

- "yellow""banana"

- "red""cherry"

Convert to a regular dict and print as By color: ◯ (printing the defaultdict directly gives a less-readable repr)

Python Editor

Run code to see output

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.

list vs. deque operation cost
listLeft-end add / removeShifts every elementby one slot→ O(n) slowdequeBoth-end append / popPointer manipulationonly→ O(1) fast
list left-end ops are O(n) (every element shifts), while deque is O(1) on both ends. With deque(maxlen=N), you get a ring buffer that automatically drops the oldest when full.
MethodEffectCost
append(x)Add to right endO(1)
appendleft(x)Add to left endO(1)
pop()Remove from right endO(1)
popleft()Remove from left endO(1)
maxlen=NSet max length (ring buffer)Opposite end drops automatically
deque core methods
appendleft(x)deque[a, b, c]append(x)popleft()pop()
Against the central deque, the left end uses appendleft / popleft and the right end uses append / pop. The arrow directions show add (into the deque) vs. remove (out of the deque).

Keep the last 3 access logs in a deque(maxlen=3). After 5 appends, only the last 3 survive.

① Import deque from collections

② Create an empty deque with max length 3

Append the integers 0 through 4 in order (5 total)

④ Print the result as Last 3: ◯ (the older 0 and 1 should have been pushed out)

Python Editor

Run code to see output

Pop the head and tail of a task queue. Use popleft and pop to switch which end you pull from.

① Import deque from collections

② Build a deque from the four tasks ["Task A", "Task B", "Task C", "Task D"]

③ Use popleft() to grab the first task and print it as First task: ◯

④ Use pop() to grab the last task and print it as Last task: ◯

⑤ Print the remaining deque as Remaining: ◯

Python Editor

Run code to see output

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 tuplesp[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.

namedtuple at a glance
Point = namedtuple('Point', ['x', 'y'])p = Point(3, 4)By namep.x / p.yBy indexp[0] / p[1]As dictp._asdict()
Define a class-equivalent in one line, then use both positional access (p[0]) and named access (p.x). _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.

Define a Point namedtuple and read coordinates by name.

① Import namedtuple from collections and create one with class name "Point" and fields ["x", "y"]

② Build an instance p with x=3, y=4

③ Print as x: 3 y: 4 using named access p.x and p.y

Python Editor

Run code to see output

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.

Take the p from Practice 1 and try the three extra access patterns.

print("repr:", p) to print as repr: Point(x=3, y=4)

② Use p[0] and p[1] for index access and print as By index: 3 4

③ Use p._asdict() to print as As dict: {'x': 3, 'y': 4}

Python Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What's the most concise way to get the per-element occurrence count of a list nums in one line?

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?