I worked through Python's core containers, list, dict, set, and the collections helpers, choosing structures by their Big-O behaviour. Replacing list membership scans with set/dict lookups cut a hot path from O(n) to O(1).

Objective & Context

Picking the wrong container is a common performance trap. This lab maps operations to complexity, list append O(1), list in O(n), dict/set lookup O(1), and uses Counter and defaultdict to simplify aggregation in automation scripts.

Learning Objectives

  • Aggregate and count with collections.Counter instead of manual loops.
  • Replace O(n) list membership scans with O(1) set lookups.
  • Group items cleanly with collections.defaultdict.
  • Predict container performance using Big-O for common operations.
  • Pick the right container for aggregation, membership, and grouping tasks.

Environment & Prerequisites

Prerequisites

  • Python 3.11 with the standard library collections module
  • A small dataset to aggregate (log lines or the sample strings used here)
  • timeit for comparing lookup performance
  • Assumed skill: loops, conditionals, and basic Big-O intuition

Step-by-Step Execution

1. Aggregate with Counter

python -c "from collections import Counter; print(Counter('mississippi').most_common(2))"

Expected output:

[('i', 4), ('s', 4)]

What this teaches: Counter does the counting loop in C and most_common() returns ranked results; the manual version needs a dict, a loop, and a sort.

2. O(1) membership with a set

python -c "allow={'a','b'}; print('a' in allow)"

Expected output:

True

What this teaches: Set membership is a hash lookup, so it stays constant time as the collection grows, while list membership scans every element.

3. Group with defaultdict

from collections import defaultdict
events = [("203.0.113.5", 80), ("203.0.113.5", 443), ("198.51.100.7", 22)]
groups = defaultdict(list)
for ip, port in events:
    groups[ip].append(port)
print(dict(groups))

Expected output:

{'203.0.113.5': [80, 443], '198.51.100.7': [22]}

What this teaches: defaultdict removes the if-key-not-present boilerplate, so grouping loops read as pure intent: append each item to its group. Converting to a plain dict for the print keeps the output readable; the grouping itself works identically either way.

How It Works

How It Works

Big-O describes how cost grows with input size. Scanning a list for membership is O(n): double the list, double the worst-case scan. A set or dict lookup hashes the key and jumps straight to the bucket, which is O(1) on average regardless of size.

That difference compounds. Checking 10,000 allow-list entries against a 100,000-item list costs up to a billion comparisons; the same checks against a set cost 10,000 hash lookups. Choosing the container is often the entire optimization.

Counter and defaultdict are dict subclasses with convenience behaviour layered on. Counter adds counting and ranking; defaultdict auto-creates missing values via a factory. Because they are dicts, everything you know about dict performance and methods still applies.

Validation & Testing

Benchmark list in versus set in on a large collection and confirm the set is dramatically faster. Pass criteria: correct aggregation output and measured O(1) lookup advantage for the set/dict path.

Advanced: Troubleshooting
  • Slow membership: repeated x in list is O(n); convert to a set once.
  • KeyError: use dict.get or defaultdict for missing keys.
  • Unhashable key: only immutable types can be dict keys or set members.

Troubleshooting

SymptomLikely causeFix
Counter on unhashable items raises TypeErrorlists or dicts cannot be dict keysconvert to a hashable form first, e.g. tuples
defaultdict creates entries on readaccessing groups[key] inserts the defaultuse .get() or check membership when probing without inserting
set lost the orderingsets are unordered by designuse dict.fromkeys() or a list plus a seen-set when order matters
dict lookup still slowkeys with expensive or colliding __hash__profile the hash; prefer small immutable keys like ints, strings, tuples
memory grew unexpectedlyCounter/defaultdict kept every distinct key of a huge streamaggregate in bounded windows or spill to disk for unbounded streams

Key Results

  • Cut a membership-check hot path from O(n) to O(1) with a set.
  • Simplified aggregation using Counter and defaultdict.
  • Mapped 4 container types to their complexity profiles.
  • Documented when each structure is the right choice.

Key Takeaways

  • Choose containers by Big-O: sets and dicts give O(1) average lookups.
  • Counter replaces hand-rolled counting loops with ranked results in one call.
  • defaultdict eliminates key-existence boilerplate in grouping code.
  • Membership testing is the classic O(n) to O(1) win: use a set.
  • All three helpers are dicts underneath, so dict semantics still apply.

Practice Challenges

Challenge 1: Hot path audit

Goal: Take a script that checks membership against a list in a loop, convert it to a set, and benchmark both with timeit on 10k items.

Hint: Build the set once outside the loop; measure only the lookup loop.

Challenge 2: Log grouper

Goal: Group web log lines by status code with defaultdict(list), then rank codes with Counter.

Hint: Parse each line once into (code, line), then feed codes to Counter separately.