The collections Module
Six specialised containers that solve problems the built-in types solve awkwardly: counting, grouping, queues, records, and layered lookups.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
What is in the box
| Type | Solves |
|---|---|
Counter | Counting occurrences |
defaultdict | Grouping and accumulating |
deque | Fast adds and removes at both ends |
namedtuple | A record with named fields |
OrderedDict | Order sensitive comparison and reordering |
ChainMap | Layered lookups such as defaults and overrides |
Counter
from collections import Counter
text = "the cat sat on the mat the end"
counts = Counter(text.split())
print(counts) # Counter({'the': 3, 'cat': 1, ...})
print(counts["the"]) # 3
print(counts["missing"]) # 0 - never raises a KeyError
print(counts.most_common(2)) # [('the', 3), ('cat', 1)]
print(sum(counts.values())) # 8 - the total count
print(list(counts.elements())[:5]) # each item repeated by its countprint(Counter("mississippi")) # per character
print(Counter([1, 1, 2, 3, 3, 3]))
print(Counter({"a": 3, "b": 1})) # from counts you already have
counts = Counter()
counts.update(["a", "b"])
counts.update(["b", "c"])
counts["d"] += 1
print(counts) # Counter({'b': 2, 'a': 1, 'c': 1, 'd': 1})Counter arithmetic
from collections import Counter
a = Counter("aabbc")
b = Counter("abbbd")
print(a + b) # counts added
print(a - b) # subtracted; zero and negative results are dropped
print(a & b) # the minimum of each - what both have
print(a | b) # the maximum of eachdef is_anagram(first, second):
normalise = lambda s: Counter(c for c in s.lower() if c.isalnum())
return normalise(first) == normalise(second)
print(is_anagram("Listen", "Silent")) # True
print(is_anagram("hello", "world")) # Falsedefaultdict
from collections import defaultdict
groups = defaultdict(list) # a missing key becomes []
for word in ["apple", "avocado", "banana"]:
groups[word[0]].append(word)
print(dict(groups))
totals = defaultdict(int) # a missing key becomes 0
for item, amount in [("pen", 10), ("book", 50), ("pen", 5)]:
totals[item] += amount
print(dict(totals))
index = defaultdict(set) # a missing key becomes set()
for name, tag in [("a", "x"), ("b", "x"), ("a", "y")]:
index[tag].add(name)
print(dict(index))
nested = defaultdict(lambda: defaultdict(int)) # two levels
nested["north"]["jan"] += 100
print(nested["north"]["jan"]) # 100Reading a missing key from adefaultdictcreates it. If you only want to look, used.get(key)instead, or convert to a plaindictbefore handing it to other code.
deque
from collections import deque
queue = deque([1, 2, 3])
queue.append(4) # add at the right
queue.appendleft(0) # add at the left
print(queue) # deque([0, 1, 2, 3, 4])
print(queue.pop()) # 4 - from the right
print(queue.popleft()) # 0 - from the left
queue.extend([5, 6])
queue.extendleft([-1, -2]) # note: reverses the order it adds
print(queue)
queue.rotate(1) # move everything one place right
print(queue)| Operation | list | deque |
|---|---|---|
| Append or pop at the right | Constant | Constant |
| Insert or pop at the left | Proportional to length | Constant |
| Index in the middle | Constant | Proportional to length |
import time
from collections import deque
n = 100_000
start = time.perf_counter()
values = []
for i in range(n):
values.insert(0, i) # every insert shifts the whole list
print(f"list: {time.perf_counter() - start:.3f}s")
start = time.perf_counter()
values = deque()
for i in range(n):
values.appendleft(i)
print(f"deque: {time.perf_counter() - start:.3f}s")A bounded deque
from collections import deque
recent = deque(maxlen=3)
for value in [1, 2, 3, 4, 5]:
recent.append(value)
print(list(recent)) # older items fall off the left automaticallyA maxlen deque is the simplest possible sliding window: keep the last N log lines, the last N readings, the last N moves for an undo history.
namedtuple
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 7)
print(p.x, p.y) # 3 7 - by name
print(p[0], p[1]) # 3 7 - still a tuple
print(p) # Point(x=3, y=7)
x, y = p # still unpacks
print(p._asdict()) # {'x': 3, 'y': 7}
print(p._replace(x=10)) # Point(x=10, y=7) - a NEW point
print(Point._fields) # ('x', 'y')Employee = namedtuple("Employee", "name role salary")
staff = [
Employee("Meera", "engineer", 90000),
Employee("Arun", "designer", 75000),
]
for member in staff:
print(f"{member.name:<8}{member.role:<12}{member.salary:>8,}")
print(max(staff, key=lambda e: e.salary).name)Compare record[2] with record.salary. Named tuples cost nothing extra and remove a whole class of index mistakes. For anything that needs methods or mutability, use a dataclass instead - covered later in this path.
OrderedDict
from collections import OrderedDict
# Since Python 3.7 a plain dict already keeps insertion order,
# so OrderedDict is now needed only for these two behaviours:
print({"a": 1, "b": 2} == {"b": 2, "a": 1}) # True
print(OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1)) # False - order matters
d = OrderedDict(a=1, b=2, c=3)
d.move_to_end("a") # push a key to the end
print(list(d)) # ['b', 'c', 'a']
d.move_to_end("c", last=False) # or to the front
print(list(d)) # ['c', 'b', 'a']
print(d.popitem(last=False)) # remove from the frontclass LRUCache:
"""A least recently used cache in about fifteen lines."""
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return None
self.data.move_to_end(key) # mark as recently used
return self.data[key]
def put(self, key, value):
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False) # drop the least recently used
cache = LRUCache(2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a")
cache.put("c", 3)
print(list(cache.data)) # ['a', 'c'] - b was evictedChainMap
from collections import ChainMap
defaults = {"theme": "light", "size": 12, "wrap": True}
user = {"size": 14}
command_line = {"theme": "dark"}
settings = ChainMap(command_line, user, defaults)
print(settings["theme"]) # dark - from command_line
print(settings["size"]) # 14 - from user
print(settings["wrap"]) # True - from defaults
print(dict(settings))Lookup walks the mappings left to right and returns the first hit. Nothing is copied, so a later change to defaults is visible immediately - which a merged dictionary would not give you.
settings["size"] = 16 # writes always go to the FIRST mapping
print(command_line) # {'theme': 'dark', 'size': 16}
print(user) # {'size': 14} - untouched
print(settings.parents["size"]) # 14 - skip the first layerChoosing between them
| You are doing | Use |
|---|---|
| Counting how often things appear | Counter |
| Grouping items under keys | defaultdict(list) |
| A queue, or work at both ends | deque |
| Keeping only the last N items | deque(maxlen=N) |
| A fixed record with named fields | namedtuple |
| Layered configuration | ChainMap |
| Order sensitive equality or reordering | OrderedDict |
Common mistakes
- Reading from a
defaultdictand accidentally creating keys. - Passing a
defaultdictout of a function, so callers inherit the auto-creation. - Indexing into the middle of a large
deque, which is not constant time. - Expecting
extendleftto preserve the order of what you add. It reverses it. - Trying to assign to a
namedtuplefield; use_replace. - Using
OrderedDictwhere a plaindictalready suffices.
Best practices
- Reach for
Counterthe moment you writecounts[x] = counts.get(x, 0) + 1. - Reach for
defaultdictthe moment you writeif key not in d: d[key] = []. - Use
dequewhenever the front of a collection is busy. - Use
namedtupleinstead of a bare tuple once there are three or more fields. - Convert a
defaultdicttodictbefore returning it.
Practice
- Find the ten most common words in a text file and their counts.
- Group a list of file paths by extension and report how many of each.
- Implement a queue simulation with
dequeand time it against a list. - Rewrite a list of five field tuples as named tuples and sort by two fields.
- Build a three layer configuration with
ChainMapand show which layer supplies each value.
Conclusion
The built in types cover most of what you need; collections covers the rest. Counter for counting, defaultdict for grouping, deque for queues and windows, namedtuple for records, ChainMap for layered settings.