Python · Syntax · Intermediate

collections: Counter, defaultdict, namedtuple

10 tasks

Specialized container types: Counter for counting, defaultdict for grouped data, namedtuple for structured records.

Counter and defaultdict

#
The `collections` module provides specialised container types that solve common problems more cleanly than plain dicts and lists. **Counter — count occurrences** ```python from collections import Counter words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'] c = Counter(words) # Counter({'apple': 3, 'banana': 2, 'cherry': 1}) c['apple'] # 3 c['mango'] # 0 — missing keys return 0, not KeyError c.most_common(2) # [('apple', 3), ('banana', 2)] c.total() # 6 (Python 3.10+) # Count characters in a string Counter('mississippi') # Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1}) ``` **Counter arithmetic** ```python a = Counter({'cat': 3, 'dog': 2}) b = Counter({'dog': 1, 'bird': 2}) a + b # Counter({'cat': 3, 'dog': 3, 'bird': 2}) — union (add counts) a - b # Counter({'cat': 3, 'dog': 1}) — subtract (drop negatives) a & b # Counter({'dog': 1}) — intersection (min counts) a | b # Counter({'cat': 3, 'bird': 2, 'dog': 2}) — union (max counts) ``` **defaultdict — automatic default values** A `defaultdict` calls a factory function to create missing values instead of raising `KeyError`: ```python from collections import defaultdict # Group words by first letter by_letter = defaultdict(list) # factory: list -> default is [] for word in ['apple', 'avocado', 'banana', 'blueberry']: by_letter[word[0]].append(word) # defaultdict({'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry']}) # Count without Counter freq = defaultdict(int) # factory: int -> default is 0 for ch in 'hello world': freq[ch] += 1 # Nested dicts graph = defaultdict(set) # adjacency list for a graph graph['A'].add('B') graph['A'].add('C') ``` **defaultdict vs dict.get vs dict.setdefault** ```python d = {} # Plain dict — three approaches to missing keys: d.get('key', 0) + 1 # read with default, but doesn't store it d.setdefault('key', []).append(1) # store default on first access # defaultdict — cleanest when all missing keys share the same default type dd = defaultdict(list) dd['key'].append(1) # no special handling needed ```

deque (with maxlen) and namedtuple

#
**deque — double-ended queue** `deque` (pronounced 'deck') supports O(1) appends and pops from both ends. A regular list has O(n) `insert(0, x)` and `pop(0)` because everything shifts. ```python from collections import deque d = deque([1, 2, 3]) d.append(4) # [1, 2, 3, 4] — add to right d.appendleft(0) # [0, 1, 2, 3, 4] — add to left, O(1) d.pop() # 4, d = [0, 1, 2, 3] d.popleft() # 0, d = [1, 2, 3] d.rotate(1) # [3, 1, 2] — rotate right by 1 d.rotate(-1) # [1, 2, 3] — rotate left by 1 ``` **maxlen — sliding window / fixed-size buffer** When a `deque` has `maxlen`, new items automatically displace old ones from the other end: ```python # Keep the last 5 commands typed history = deque(maxlen=5) for cmd in ['ls', 'cd /tmp', 'cat file', 'pwd', 'ls -la', 'whoami']: history.append(cmd) list(history) # ['cd /tmp', 'cat file', 'pwd', 'ls -la', 'whoami'] # 'ls' was dropped when 'whoami' arrived because maxlen=5 # Sliding window average window = deque(maxlen=3) for reading in [10, 20, 30, 40, 50]: window.append(reading) print(sum(window) / len(window)) # 10.0, 15.0, 20.0, 30.0, 40.0 ``` **namedtuple — lightweight record type** ```python from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) p = Point(3, 4) p.x # 3 — attribute access p[0] # 3 — index access still works (it's a tuple) p.x, p.y # 3, 4 x, y = p # unpacking still works ``` **Useful namedtuple methods** ```python Point._fields # ('x', 'y') Point._make([3, 4]) # Point(x=3, y=4) from any iterable p._replace(x=10) # Point(x=10, y=4) — returns a new instance p._asdict() # {'x': 3, 'y': 4} ``` **namedtuple vs dict vs dataclass** ``` namedtuple — immutable, tuple-compatible, very memory-efficient dict — mutable, flexible keys, slightly more memory dataclass — mutable by default, supports methods, type hints, __post_init__ ``` If the fields are fixed and you don't need mutation, `namedtuple` is the lightest choice.

OrderedDict, ChainMap, and choosing the right collection

#
**OrderedDict — dict that remembers insertion order** In Python 3.7+, regular `dict` also preserves insertion order, so `OrderedDict` is rarely needed. It's still useful for its `.move_to_end()` method and for when you explicitly want to signal ordering matters: ```python from collections import OrderedDict od = OrderedDict() od['a'] = 1 od['b'] = 2 od['c'] = 3 od.move_to_end('a') # move 'a' to the end list(od) # ['b', 'c', 'a'] od.move_to_end('c', last=False) # move 'c' to the front list(od) # ['c', 'b', 'a'] # LRU cache (evict least recently used) class LRUCache: def __init__(self, capacity): self.cache = OrderedDict() self.cap = capacity def get(self, key): if key not in self.cache: return -1 self.cache.move_to_end(key) # mark as recently used return self.cache[key] def put(self, key, value): self.cache[key] = value self.cache.move_to_end(key) if len(self.cache) > self.cap: self.cache.popitem(last=False) # evict oldest ``` **Choosing the right collection** ``` Need Use ──────────────────────────────────── ───────────────────── Count occurrences Counter Group items, auto-init missing keys defaultdict(list) Accumulate, no KeyError on new keys defaultdict(int/float) Fast queue (both ends) deque Fixed-size sliding window deque(maxlen=N) Lightweight immutable record namedtuple Mutable record with methods dataclass Dict with explicit ordering control OrderedDict Everything else dict / list ``` **ChainMap — overlay multiple dicts without copying** ```python from collections import ChainMap defaults = {'color': 'red', 'size': 'M'} overrides = {'color': 'blue'} merged = ChainMap(overrides, defaults) merged['color'] # 'blue' — found in overrides first merged['size'] # 'M' — falls through to defaults # Writes go to the first map merged['weight'] = 'heavy' overrides # {'color': 'blue', 'weight': 'heavy'} ``` Useful for config layering (user settings override defaults) and for scoped variable lookups.
01

#

Use `Counter` to count how many times each word appears in a list. Return the Counter object.

from collections import Counter

def word_count(words):
    # your code here
    pass

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
c = word_count(words)
print(c['apple'])   # 3
print(c['banana'])  # 2
print(c['grape'])   # 0 (Counter returns 0 for missing keys)
Solution
from collections import Counter

def word_count(words):
    return Counter(words)
02

#

Return the 3 most common words in a list of words.

from collections import Counter

def top_three(words):
    # your code here
    pass

words = ['the', 'cat', 'sat', 'on', 'the', 'mat', 'the', 'cat', 'is', 'fat']
print(top_three(words))  # [('the', 3), ('cat', 2), ('sat', 1)] or similar
Solution
from collections import Counter

def top_three(words):
    return Counter(words).most_common(3)
03

#

Group a list of words by their first letter using `defaultdict`. Return a dict where each key is a letter and the value is a list of words.

from collections import defaultdict

def group_by_letter(words):
    # your code here
    pass

words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry']
result = group_by_letter(words)
print(result['a'])  # ['apple', 'avocado']
print(result['b'])  # ['banana', 'blueberry']
Solution
from collections import defaultdict

def group_by_letter(words):
    groups = defaultdict(list)
    for word in words:
        groups[word[0]].append(word)
    return dict(groups)
04

#

Use `defaultdict(int)` to count letter frequencies in a string. Return the defaultdict.

from collections import defaultdict

def letter_freq(s):
    # your code here
    pass

freq = letter_freq('hello')
print(freq['l'])  # 2
print(freq['h'])  # 1
print(freq['z'])  # 0
Solution
from collections import defaultdict

def letter_freq(s):
    freq = defaultdict(int)
    for c in s:
        freq[c] += 1
    return freq
05

#

Use `OrderedDict` to create a cache that keeps the last 3 unique items added (LRU-like). Implement `add(key, value)` and `get_all()` methods.

from collections import OrderedDict

class SmallCache:
    def __init__(self):
        self.cache = OrderedDict()
        self.max_size = 3

    def add(self, key, value):
        # your code here
        pass

    def get_all(self):
        return list(self.cache.items())

c = SmallCache()
c.add('a', 1)
c.add('b', 2)
c.add('c', 3)
c.add('d', 4)  # 'a' should be evicted
print(c.get_all())  # [('b', 2), ('c', 3), ('d', 4)]
Solution
from collections import OrderedDict

class SmallCache:
    def __init__(self):
        self.cache = OrderedDict()
        self.max_size = 3

    def add(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.max_size:
            self.cache.popitem(last=False)  # remove oldest

    def get_all(self):
        return list(self.cache.items())
06

#

Use a `deque` to implement a sliding window maximum: given a list of numbers and window size k, return a list of the maximum value in each window.

from collections import deque

def sliding_max(nums, k):
    # your code here
    pass

print(sliding_max([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]
Solution
from collections import deque

def sliding_max(nums, k):
    result = []
    window = deque()  # stores indices
    for i, n in enumerate(nums):
        while window and nums[window[-1]] <= n:
            window.pop()
        window.append(i)
        if window[0] <= i - k:
            window.popleft()
        if i >= k - 1:
            result.append(nums[window[0]])
    return result
07

#

Use `namedtuple` to create a `Point` type with `x` and `y` fields. Return a function that computes the distance between two Points.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

def distance(p1, p2):
    # your code here
    pass

a = Point(0, 0)
b = Point(3, 4)
print(distance(a, b))  # 5.0
Solution
from collections import namedtuple
import math

Point = namedtuple('Point', ['x', 'y'])

def distance(p1, p2):
    return math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
08

#

Given two Counters (word frequencies from two texts), return a new Counter with the combined word frequencies.

from collections import Counter

def combine_frequencies(c1, c2):
    # your code here
    pass

c1 = Counter({'apple': 3, 'banana': 1})
c2 = Counter({'apple': 2, 'cherry': 4})
result = combine_frequencies(c1, c2)
print(result['apple'])   # 5
print(result['banana'])  # 1
print(result['cherry'])  # 4
Solution
from collections import Counter

def combine_frequencies(c1, c2):
    return c1 + c2
09

#

Use `deque` with `maxlen` to keep only the last N items added to a stream. Implement a `StreamBuffer` class with `push(item)` and `get_recent()` methods.

from collections import deque

class StreamBuffer:
    def __init__(self, maxlen):
        # your code here
        pass

    def push(self, item):
        # your code here
        pass

    def get_recent(self):
        return list(self.buffer)

buf = StreamBuffer(3)
for x in [1, 2, 3, 4, 5]:
    buf.push(x)
print(buf.get_recent())  # [3, 4, 5]
Solution
from collections import deque

class StreamBuffer:
    def __init__(self, maxlen):
        self.buffer = deque(maxlen=maxlen)

    def push(self, item):
        self.buffer.append(item)

    def get_recent(self):
        return list(self.buffer)
10

#

Given a list of transactions (each a dict with 'category' and 'amount'), use `defaultdict` to compute the total amount per category.

from collections import defaultdict

def totals_by_category(transactions):
    # your code here
    pass

txns = [
    {'category': 'food',    'amount': 12.5},
    {'category': 'travel',  'amount': 200.0},
    {'category': 'food',    'amount': 8.0},
    {'category': 'travel',  'amount': 50.0},
    {'category': 'books',   'amount': 25.0},
]
result = totals_by_category(txns)
print(result['food'])    # 20.5
print(result['travel'])  # 250.0
Solution
from collections import defaultdict

def totals_by_category(transactions):
    totals = defaultdict(float)
    for t in transactions:
        totals[t['category']] += t['amount']
    return dict(totals)