The collections Module

Six specialised containers that solve problems the built-in types solve awkwardly: counting, grouping, queues, records, and layered lookups.

What is in the box

TypeSolves
CounterCounting occurrences
defaultdictGrouping and accumulating
dequeFast adds and removes at both ends
namedtupleA record with named fields
OrderedDictOrder sensitive comparison and reordering
ChainMapLayered 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 count
print(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 each
def 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"))            # False

defaultdict

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"])              # 100
Reading a missing key from a defaultdict creates it. If you only want to look, use d.get(key) instead, or convert to a plain dict before 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)
Operationlistdeque
Append or pop at the rightConstantConstant
Insert or pop at the leftProportional to lengthConstant
Index in the middleConstantProportional 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 automatically

A 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 front
class 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 evicted

ChainMap

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 layer

Choosing between them

You are doingUse
Counting how often things appearCounter
Grouping items under keysdefaultdict(list)
A queue, or work at both endsdeque
Keeping only the last N itemsdeque(maxlen=N)
A fixed record with named fieldsnamedtuple
Layered configurationChainMap
Order sensitive equality or reorderingOrderedDict

Common mistakes

  • Reading from a defaultdict and accidentally creating keys.
  • Passing a defaultdict out of a function, so callers inherit the auto-creation.
  • Indexing into the middle of a large deque, which is not constant time.
  • Expecting extendleft to preserve the order of what you add. It reverses it.
  • Trying to assign to a namedtuple field; use _replace.
  • Using OrderedDict where a plain dict already suffices.

Best practices

  • Reach for Counter the moment you write counts[x] = counts.get(x, 0) + 1.
  • Reach for defaultdict the moment you write if key not in d: d[key] = [].
  • Use deque whenever the front of a collection is busy.
  • Use namedtuple instead of a bare tuple once there are three or more fields.
  • Convert a defaultdict to dict before returning it.

Practice

  1. Find the ten most common words in a text file and their counts.
  2. Group a list of file paths by extension and report how many of each.
  3. Implement a queue simulation with deque and time it against a list.
  4. Rewrite a list of five field tuples as named tuples and sort by two fields.
  5. Build a three layer configuration with ChainMap and 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.