Exercises — Standard library — math, random, os, sys, datetime, time, collections, itertools, functools
Before we start, one shared vocabulary reminder (nothing here is used before it is defined):
Level 1 — Recognition
Goal: can you name the right drawer and the right tool?
L1.1 Which single standard-library module provides Counter, defaultdict, deque, and namedtuple?
Recall Solution
collections. Mental model: it is the drawer of "containers with superpowers". Counter counts, defaultdict fills missing keys, deque is a fast two-ended queue, namedtuple gives tuples named fields.
L1.2 You must compute (choose 3 from 8, order ignored) as an exact integer. Which module and function?
Recall Solution
math.comb(8, 3). The word "choose, order ignored, exact integer" points straight at math.comb. It returns the binomial coefficient with no floating-point rounding.
L1.3 You want to measure how long a piece of code takes to run. Which function — time.time() or time.perf_counter()?
Recall Solution
time.perf_counter(). It is the highest-resolution monotonic clock (never jumps backward if the system clock is adjusted). time.time() gives wall-clock seconds-since-1970, which can shift.
Level 2 — Application
Goal: call the tool correctly and predict its output.
L2.1 Predict the exact output:
from collections import Counter
print(Counter("mississippi").most_common(2))Recall Solution
Tally the letters of "mississippi": m=1, i=4, s=4, p=2.
most_common(2) returns the two highest counts as (item, count) pairs, highest first, ties broken by first-seen order.
Since i and s both have count 4, and i appears first in insertion order, the output is:
[('i', 4), ('s', 4)]L2.2 What does this print, and why is the upper bound reached?
import random
random.seed(0)
print([random.randint(1, 3) for _ in range(4)])Recall Solution
random.randint(a, b) is inclusive on both ends, so each draw is one of {1, 2, 3}. Because we called random.seed(0), the sequence is deterministic and reproducible. With CPython's generator, seed 0 yields:
[3, 3, 1, 2]The value 3 can appear precisely because the top bound is inclusive (unlike range).
L2.3 Build a portable path to data/raw/log.csv and print it. Which one line?
Recall Solution
import os
print(os.path.join('data', 'raw', 'log.csv'))On Linux/macOS this prints data/raw/log.csv; on Windows data\raw\log.csv. We use os.path.join (not string +) because the separator differs per operating system, and join picks the right one automatically.
Level 3 — Analysis
Goal: reason about sizes, complexity, and why one tool beats another.
L3.1 Using the size formulas from the parent note, how many elements are in each?
(a) product([0,1], repeat=3) (b) permutations('ABCD', 2) (c) combinations('ABCD', 2)
Recall Solution
Recall the derivations (see Combinatorics):
product(A, repeat=3)is all ordered triples from a 2-element set .permutations(seq, r)= ordered, no repeats .combinations(seq, r)= unordered .
Notice permutations = combinations : . Order (the internal arrangements) is the only difference. See the figure below.

L3.2 You maintain a queue of tasks. You repeatedly remove from the front. Compare the cost of list.pop(0) versus collections.deque.popleft() and justify with Big-O.
Recall Solution
A Python list stores items in one contiguous block. Removing index 0 means every remaining item shifts left by one slot — that is moves, i.e. per removal (and to drain the whole queue).
A deque is a doubly-linked structure of blocks; popleft() just detaches the front end — per removal, to drain.
Conclusion: for a front-removed queue, deque is asymptotically faster. The figure contrasts the two.

L3.3 Naive recursive fib(n) makes how many calls, roughly, and what does @lru_cache change it to? Give both in Big-O and explain the mechanism.
Recall Solution
Naive fib(n) = fib(n-1) + fib(n-2) re-computes the same subproblems over and over. The call count grows like the Fibonacci numbers themselves, which grow exponentially: where — usually quoted as .
@lru_cache(maxsize=None) stores each computed fib(k) once; a repeat call is a dictionary lookup. So each of the values is computed a single time time. This is Recursion and Memoization in one decorator line. LRU = least-recently-used items are evicted first when the cache is bounded.
Level 4 — Synthesis
Goal: combine two or three drawers into one working idea.
L4.1 Given words = ['pie', 'pan', 'sun', 'sky', 'apt'], group them by their first letter into a dict-like structure, using collections. Output the grouping.
Recall Solution
from collections import defaultdict
words = ['pie', 'pan', 'sun', 'sky', 'apt']
groups = defaultdict(list)
for w in words:
groups[w[0]].append(w) # missing key auto-creates an empty list
print(dict(groups))Output:
{'p': ['pie', 'pan'], 's': ['sun', 'sky'], 'a': ['apt']}We pass the factory list (uncalled). The first time a new first-letter key is touched, defaultdict calls list() to make [], then we append — no KeyError, no if key not in d boilerplate.
L4.2 Using functools.reduce, compute the product (i.e. ) in one expression, and state the fold order explicitly.
Recall Solution
from functools import reduce
result = reduce(lambda a, b: a * b, [1, 2, 3, 4, 5]) # 120reduce folds left to right:
This equals , matching math.factorial(5).
L4.3 You want the top-3 most frequent words across two lists without merging them into a new giant list first. Use itertools.chain + collections.Counter.
Recall Solution
from itertools import chain
from collections import Counter
a = ['red', 'red', 'blue']
b = ['blue', 'green', 'red']
counts = Counter(chain(a, b)) # chain streams both, no merged list built
print(counts.most_common(3))chain(a, b) yields items lazily from a then b (an iterator), which Counter consumes directly. Tally: red=3, blue=2, green=1. Output:
[('red', 3), ('blue', 2), ('green', 1)]Level 5 — Mastery
Goal: full mini-problems that fuse behavior, complexity, and pitfalls.
L5.1 Write a memoized decorator by hand (no lru_cache) that also uses functools.wraps, and use it on fib. Then explain what wraps preserves. (See Decorators.)
Recall Solution
from functools import wraps
def memoize(f):
cache = {}
@wraps(f) # keep f's name/docstring on the wrapper
def wrapper(n):
if n not in cache:
cache[n] = f(n)
return cache[n]
return wrapper
@memoize
def fib(n):
"""nth Fibonacci number."""
return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(10)) # 55
print(fib.__name__) # 'fib' (thanks to @wraps)fib(10) = 55. Without @wraps, fib.__name__ would read 'wrapper' and the docstring would be lost — wraps copies the original function's metadata onto the wrapper so tools and help() still see the real fib.
L5.2 A file nums.txt holds one integer per line. Read it, and report both the sum and the most common value. Combine File Handling in Python with collections.
Recall Solution
from collections import Counter
with open('nums.txt') as fh:
nums = [int(line) for line in fh] # strip+int per line
total = sum(nums)
most = Counter(nums).most_common(1)[0] # (value, count) pair
print(total, most)For a file containing the lines 3, 3, 5, 7, 3, 5: nums = [3,3,5,7,3,5], total = 26, and Counter(...).most_common(1) = [(3, 3)], so most = (3, 3) (the value 3 occurred 3 times). The with block guarantees the file closes even on error.
L5.3 Time two ways of concatenating 5 sublists into one flat list — repeated + versus itertools.chain — conceptually. Which grows worse, and why, in Big-O?
Recall Solution
from itertools import chain
flat = list(chain([1,2],[3,4],[5,6],[7,8],[9,10])) # [1,2,...,10]- Repeated
+(a + b + c + ...) builds a new list each step, copying all elements accumulated so far. If there are chunks each of size , the copying cost is — quadratic in the number of chunks. chain(...)produces a single lazy stream; wrapping once inlist(...)copies each element exactly once total (linear).
The flat result is [1,2,3,4,5,6,7,8,9,10] (sum ). chain wins because it avoids re-copying intermediate results.
Recall Self-test summary (cloze)
Choose 3 from 8, exact integer ::: math.comb(8, 3)
Front-removal queue, both ends ::: collections.deque (popleft/appendleft)
Turns naive fib into ::: functools.lru_cache (memoization)
permutations(n, r) ÷ combinations(n, r) equals :::
Passed to defaultdict — value or factory? ::: the factory (list, not list())