The Structures Python Ships But Never Advertises
Counting how often each word appears takes four lines and one conditional that everybody eventually gets wrong. Swap in the structure built for exactly that job and watch the same tally come out of a single line.
Worth reading first: Looking Things Up Instead of Counting Along
Counting how often each word appears takes four lines and one conditional that everybody eventually gets wrong. Swap in the structure built for exactly that job and watch the same tally come out of a single line.
Counter does the tally you keep rewriting
Tallying with a plain dictionary is a rite of passage, and the shape of it is always the same: check whether the key is there, start it at one if not, add one if so.
counts = {}for colour in colours: if colour in counts: counts[colour] += 1 else: counts[colour] = 1Six lines that do one thing, and the conditional exists solely because a missing key raises rather than starting at zero. Counter is a dictionary that has already decided a missing key means zero.
from collections import Counter counts = Counter(colours)counts.most_common(2) # [('red', 4), ('blue', 3)]counts["magenta"] # 0, not a KeyErroritems = ["red", "blue", "red", "green", "blue", "red", "amber", "blue", "red"]
>>> Counter(items).most_common()
[('red', 4), ('blue', 3), ('green', 1), ('amber', 1)]
most_common() hands back the pairs already ordered, so the winner is rows[0] rather than something you have to search for. Every count here came from one call — no loop, no conditional, and no key that had to exist first.
defaultdict stops you checking before every write
Counter handles counting. defaultdict generalises the idea: you hand it a function, and any key you read that does not exist is created by calling it.
from collections import defaultdict by_track = defaultdict(list)for student in students: by_track[student["track"]].append(student["name"]) # defaultdict(<class 'list'>, {'python': ['Amara', 'Chidi'], 'ml': ['Ben']})You pass list, not list() — the type itself, so the dictionary can call it fresh for each new key. Passing list() would hand over one already-built list, and every key would end up sharing it, which is the aliasing trap from the previous chapter in one of its least obvious costumes.
namedtuple gives a tuple field names
A tuple is the right shape for a fixed group of values, and the wrong shape for remembering which position means what. namedtuple keeps the tuple and adds the names.
from collections import namedtuple Point = namedtuple("Point", ["x", "y"])p = Point(3, 4) p.x # 3p[0] # 3 — still an ordinary tuple underneathx, y = p # still unpacksReach for namedtuple
A small, fixed group of values that will not change after it is built, and that you want to read by name. Coordinates, RGB colours, a parsed row.
Reach for a class
The thing needs methods, needs to change after creation, or has enough fields that positional construction stops being readable. That is what classes are for.
deque is fast at the end a list is slow at
A list is quick to append to and quick to read by index. It is slow at exactly one thing: removing from or inserting at the front, because every remaining item has to shuffle down one position to close the gap.
from collections import deque queue = deque(["a", "b", "c"])queue.appendleft("start") # cheap on a deque, costly on a listqueue.popleft() # cheap on a deque, costly on a listOn a hundred items nobody notices. On a queue of a hundred thousand, processed front to back, a list turns a linear job into a quadratic one — and the program does not fail, it just gets slower in a way that looks like the data got bigger.
These are ordinary imports, not language features
Nothing in this chapter is built into the language. collections is a module in the standard library, written in Python and C like any other, and every structure in it could be built out of the dictionaries and lists you already have.
That is worth stating plainly, because it removes the mystery. A Counter is a dictionary subclass that overrides what a missing key means. A defaultdict is a dictionary subclass that calls a function on a miss. You could write both, and reading their source is a genuinely good way to spend twenty minutes.
What you get by importing them instead is that they are already correct, already fast, and already familiar to whoever reads your code next.
Key takeaways
- Counter is a dictionary where a missing key counts as zero, which removes the conditional every hand-written tally needs.
- most_common() returns the pairs already ordered, so the winner is the first item rather than something you search for.
- defaultdict takes a function and calls it for any key you read that does not exist. Pass list, not list() — the type, not an instance.
- Reading a missing key on a defaultdict creates it. Merely looking changes the dictionary, so use .get() to check without writing.
- namedtuple keeps a tuple's behaviour and adds field names. Use it for small fixed groups; use a class once behaviour or mutation is involved.
- A list is slow at the front, because removing the first item shuffles every other item down. deque is fast at both ends.
- deque(maxlen=n) gives you a rolling window of the last n items for free.
- None of these are language features. They are a standard-library module, and every one could be built from a dictionary you already know how to write.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.