Dinesh/ Blog
algorithms

Python for Speed — The 20 Idioms That Matter

The small set of standard-library tools that turn a fifteen-line design review solution into a five-line one, and the syntax you must be able to type without thinking.

15 min readChecking device speech…
Lesson preparation & details

Level: beginner

By the end, you should be able to

  • Counter/defaultdict missing-key behavior and deque end operations are library contracts

Bring with you

Sources checked: . A source check does not change the publication or update date.

Editorial review: · What review means

In this article · 16 sections

Review boundary and corrected contracts

Counter/defaultdict missing-key behavior and deque end operations are library contracts. Top-k requires a legal k; heap storage is additional to the complete frequency map. The anagram vector assumes lowercase ASCII, unlike the sorted-character key.

The examples below teach the stated input contracts; fragments and deliberately incorrect counterexamples are exercises, not production implementations. Complexity counts elementary operations unless a bit-cost or allocation caveat is stated. Practice times are suggestions, not measured performance or hiring guarantees.

Learning objective: Build a small, reliable Python vocabulary — Counter, defaultdict, heapq, deque, comprehensions, enumerate/zip — so the mechanical parts of a solution take seconds and your thinking goes to the algorithm.

Series: LeetCode — From Basics to Production-Ready · Session 3 / 65 · Phase 0 · Foundations

Prerequisites: L02 (expected set lookup versus worst-case linear list membership, under bounded-cost keys). This session is the toolbox that puts those costs at your fingertips.

What this session IS — a vocabulary, not a language

You already know Python's syntax. This session is about a small vocabulary of standard-library tools that come up in almost every practice problems. The point is fluency: when the algorithm calls for "count the letters," your hand should type Counter(s) before your brain finishes the sentence. Every second you spend fighting syntax is a second not spent on the actual problem.

The naive approach is to reinvent these each time — a manual dict with if key in d: d[key] += 1 else: d[key] = 1. It works, but it is four lines of boilerplate that can hide a bug, and it costs design review minutes. The idioms below collapse those to one line each.

Real: A carpenter does not whittle a new screwdriver for each screw. They reach for the one in the belt without looking. Fluency with a few tools beats owning a hundred you fumble.

You will not memorise all of Python. You will make about twenty idioms automatic — Counter, defaultdict, heapq, deque, sorted-with-key, enumerate, zip — and reach for them without looking.

The 20 idioms — the toolbox

Counting and grouping

from collections import Counter, defaultdict
 
nums = [2, 2, 1]
key, x = "group", 7
Counter("aabbbc")            # Counter({'b': 3, 'a': 2, 'c': 1}) — counts in one call
Counter(nums).most_common(2) # the 2 most frequent (value, count) pairs, sorted
 
d = defaultdict(list)        # missing keys auto-create an empty list
d[key].append(x)             # no "if key not in d" guard needed
 
d2 = defaultdict(int)        # missing keys default to 0
d2[key] += 1                 # a manual Counter

A missing Counter key reads as zero without insertion; explicitly storing zero does not delete a key. In Python 3.10+, equality treats missing counts as zero. For defaultdict, only indexing a missing key invokes its factory and inserts a value: d.get(missing) and missing in d do not. That side effect can matter in read-only code.

Heaps (priority queues)

import heapq
h = []
heapq.heappush(h, 3)         # O(log n) insert
heapq.heappush(h, 1)
heapq.heappop(h)             # 1 — always the SMALLEST (min-heap), O(log n)
 
h = []                      # a separate heap: do not mix encodings
x = 7
heapq.heappush(h, -x)        # push NEGATIVES to simulate a MAX-heap
assert -heapq.heappop(h) == 7 # negate again on the way out
 
nums = [4, 1, 3, 2]
heapq.nlargest(3, nums)      # top 3 without managing the heap yourself
heapq.heapify(nums)          # turn a list into a heap in place, O(n)

Deques (both-ends queues)

from collections import deque
q = deque([1, 2, 3])
q.append(4)                  # O(1) right
q.appendleft(0)              # O(1) left  (a list's insert(0, x) is O(n)!)
q.popleft()                  # O(1) — this is your BFS queue

For a FIFO queue such as a BFS frontier, enqueue new items with append on the right and process the oldest with popleft on the left. A list's pop(0) instead shifts all remaining references one position left: removing from a queue of length m costs O(m), whereas deque end operations cost O(1) under the ordinary bounded-cost model. appendleft is available, but using it for new BFS work would not preserve this FIFO order.

For a window whose items arrive in increasing index order, append each (index, value) on the right. As the window's left boundary L advances, repeatedly remove the leftmost item with popleft while its index is less than L; the remaining queue contains only unexpired items in arrival order. For a fixed width k > 0 at index i, L = max(0, i - k + 1): with k=3, arrival at i=3 expires index 0 and leaves indices 1, 2, 3. Each item is appended once and removed at most once, so end-operation bookkeeping is O(n) over n arrivals. This is window retention, not by itself a window-maximum algorithm. Use explicit expiry when removal must update other state; a bounded deque's automatic discard does not perform that bookkeeping for you.

Comprehensions and iteration helpers

nums = [2, -1, 3]
pairs = [("a", 2), ("b", 1)]
list1, list2 = [1, 2], [3, 4]
words = ["a", "longer"]
[x * x for x in nums if x > 0]        # filter + map in one line
{x for x in nums}                     # a set comprehension (dedupe)
{k: v for k, v in pairs}              # a dict comprehension
 
for i, x in enumerate(nums):          # index AND value, no range(len(...))
    ...
for a, b in zip(list1, list2):        # walk two lists in lockstep
    ...
 
sorted(words, key=len)                # sort by a computed key
sorted(pairs, key=lambda p: (-p[1], p[0]))  # by count desc, then name asc

zip stops at the shortest input by default. If unequal lengths are an error, use zip(list1, list2, strict=True) on Python 3.10+ and consume it to detect the mismatch. Comprehensions allocate their outputs; sorted returns a new list.

The little sharp tools

parts = ["a", "bc"]
a, b = 1, 2
nums = [1, 2, 3]
"".join(parts)               # O(total characters + number of pieces)
a, b = b, a                  # swap without a temp
min(nums), max(nums), sum(nums)      # one-pass aggregates, O(n)
float("inf"), float("-inf")          # sentinels for running min/max
divmod(17, 5)                # (3, 2) — quotient and remainder together
nums[::-1]                   # reversed copy;  s[i:j:2] strided slice

Why learn the idioms? Familiar tools can reduce boilerplate, but library calls are not automatically complexity-optimal or correct for a new contract. A manual running maximum is appropriate for a stream; max() is convenient for an existing iterable. A 26-counter vector is useful for a fixed alphabet; Counter handles a general hashable alphabet. State why the selected tool fits, not how few lines it saves.

These examples use bounded numeric values. Empty min/max calls raise unless given a default; sum has a zero default. Join expects strings, slices allocate copies, and numeric heap negation does not generalize to arbitrary objects. For equal-priority tasks whose payloads cannot be compared, use a separate comparable tie-breaker such as a sequence number.

The template — a solution skeleton using the vocabulary

This executable composition returns both groups of input indices and the most frequent values. It is an example of combining tools, not a template to paste into every array problem. Input is a reusable sequence of integers; group_key returns hashable keys; 1 <= k <= number of distinct values.

def solve(nums, k, group_key):
    from collections import Counter, defaultdict
    import heapq
 
    counts = Counter(nums)
    groups = defaultdict(list)
    for i, x in enumerate(nums):
        groups[group_key(x)].append(i)
 
    heap = []
    for key, val in counts.items():
        heapq.heappush(heap, (val, key))
        if len(heap) > k:
            heapq.heappop(heap)
 
    return dict(groups), [key for val, key in heap]

Counting and grouping cost expected O(n) if hashes and group_key are bounded-cost; the heap adds O(u log(k+1)) for u distinct values, with O(n+u+k) storage. Heap output is not sorted. Remove stages not required by the actual task rather than paying for every structure.

Name: Summarise, group, prioritise, shape

Picture: Four available operations: Counter summarizes frequencies, defaultdict groups, heapq prioritizes, and comprehensions shape output. Their order and usefulness depend on the task; they are not an algorithmic proof.

  • Counting letters/frequencies -> Counter.

  • Bucketing by a computed key -> defaultdict(list).

  • Top-K / smallest-K / running priority -> heapq (negate for max).

  • BFS / sliding window queue -> deque, never list.pop(0).

  • Building strings -> join for portable linear construction; count total characters, not only pieces.

Trigger: When writing repeated container bookkeeping, compare the corresponding idiom’s contract. Keep manual code when it makes required state or validation clearer.

Memory technique — "CHeD-Sez", one peg per tool

Make the five workhorses a five-letter peg you can recite: C-H-D-S-Z →

  • Counter — count.
  • Heapq — top-K / priority.
  • Defaultdict — group.
  • Sorted(key=…) — order by a rule.
  • Zip / enumerate — walk pairs / index+value.

Say "CHeD-Sez" (like a name) and each letter unpacks to a tool and its job. Because they map to the four-step model — summarise (C), prioritise (H), group (D), order/shape (S, Z) — recalling the peg recalls the whole workflow. Pair each with its one canonical line (Counter(s), heapq.heappop, d[k].append, sorted(x, key=…), enumerate(x)) and drill until you type them without thought.

Myth: heapq gives me a max-heap if I want the largest elements.

Truth: The portable heapq operations used here are min-heap operations. Python 3.14 adds public max-heap APIs; negating numeric priorities remains a compatible technique on earlier versions.

Why: There is no max-heap flag. heappop returns the smallest item, so to get the largest you invert the sign so the largest value becomes the smallest negative — which the min-heap then surfaces first.

Test: Push -3, -1, -7 and pop: you get -7 first; negate to 7, the true maximum. If you pushed 3, 1, 7 you would get 1 first — the minimum, not what you wanted.

What reviewers actually ask

These are the problems where the right idiom is most of the solution — recognising which tool to reach for is exactly the skill being tested:

  • Valid Anagram (242) · Practice selection. (Easy) — probes: do you reach for Counter(s) == Counter(t) instead of hand-rolling counts. Follow-up: "what if inputs are unicode / streaming?"
  • Group Anagrams (49) · Practice selection. (Medium) — probes: defaultdict(list) keyed by the sorted string (or a char-count tuple). Follow-up: "avoid the O(k log k) sort key" → use a 26-length count tuple.
  • Top K Frequent Elements (347) · Practice selection. (Medium) — probes: a frequency map plus top-k selection or frequency buckets; include the full map, not just heap memory. Follow-up: "can you do it in O(n)?"
  • K Closest Points to Origin (973) · Practice selection. (Medium) — probes: a size-K max-heap of negative distances. Follow-up: "streaming points, unknown total."
  • Number of Islands (200) · Practice selection. (Medium) — probes: deque for BFS flood fill (or recursion for DFS). Follow-up: "very large grid → BFS to avoid recursion limits."
  • Task Scheduler (621) · Practice selection. (Medium) — probes: Counter of task frequencies plus greedy scheduling from the most frequent.

Worked example 1 — Top K Frequent, the idiomatic way

Problem in plain words: For a sequence of integers and 1 <= k <= u, where u is the number of distinct values, return any k values with highest frequencies in any order. Inputs with k=0, k>u or no values are outside these two functions’ contract. Different choices among equal-frequency boundary values are allowed.

Baseline cost. Count n inputs in expected O(n), then sort u frequency entries in O(u log u) and select k. A bounded heap replaces sorting with O(u log(k+1)); buckets use O(n+u) work but allocate n+1 buckets. “Faster” depends on u, k, n and memory costs; n and u are not interchangeable.

Insight + heap version. Keep a heap of at most k elements; the smallest count sits at the top and gets evicted when a bigger one arrives.

from collections import Counter
import heapq
 
def top_k_frequent(nums, k):
    counts = Counter(nums)                 # O(n): {value: frequency}
    heap = []                              # min-heap of (count, value)
    for value, count in counts.items():
        heapq.heappush(heap, (count, value))
        if len(heap) > k:                  # keep only the k largest counts
            heapq.heappop(heap)            # evict the smallest -> O(log k)
    return [value for count, value in heap]
# Expected time O(n + u * log(k + 1)); O(u + k) storage, u distinct values.

Alternative: frequency buckets. Positive counts range from 1..n, so index buckets by count. This spends O(n) bucket storage even if u is small:

def top_k_frequent_bucket(nums, k):
    counts = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]   # buckets[c] = values with frequency c
    for value, c in counts.items():
        buckets[c].append(value)
    result = []
    for c in range(len(buckets) - 1, 0, -1):       # walk high frequency -> low
        for value in buckets[c]:
            result.append(value)
            if len(result) == k:
                return result
# Time O(n), space O(n).

Invariant and complexity. After processing a frequency entry, the heap retains up to k greatest (count, value) pairs seen so far: pushing adds the new candidate and popping the minimum removes a candidate outside that retained set. This takes expected O(n + u log(k+1)) time including counting, and O(u+k) storage. Buckets visit frequency levels from greatest to least, so their first k outputs satisfy the same any-tie contract in expected O(n+u) time and O(n+u) storage. These bounds assume bounded-cost integer hashing/comparison.

Follow-up: define tie direction and output order. The shown (count, value) heap prefers larger values at the cutoff and returns heap order, not rank order. To prefer smaller integer values, store (count, -value) instead and decode the value on output. Sort the selected values separately if ranked output is required. The bucket version preserves first-seen order within a frequency bucket; sort that bucket for a different tie policy.

Goal: Prove the max-heap negation trick.

Hint: Get the 3 LARGEST numbers using a MIN-heap of negatives.

import heapq
nums = [5, 1, 8, 3, 9, 2]
h = []
for x in nums:
    heapq.heappush(h, -x)          # store negatives
largest3 = [-heapq.heappop(h) for _ in range(3)]   # pop 3, negate back
print(largest3)                    # [9, 8, 5]

Now redo it with heapq.nlargest(3, nums) and confirm you get the same answer with none of the negation bookkeeping.

Worked example 2 — Group Anagrams with defaultdict

Problem in plain words: Group strings having the same multiset of Unicode code points. This is case-sensitive and does not normalize canonically equivalent Unicode forms or combine grapheme clusters. The sorted-key version accepts arbitrary strings, including empty ones.

Insight. Two words are anagrams iff they share the same letter multiset. Use that multiset as a dictionary key; defaultdict(list) collects the groups with no guard code.

from collections import defaultdict
 
def group_anagrams(words):
    groups = defaultdict(list)
    for w in words:
        key = tuple(sorted(w))       # ('a','e','t') for "ate","eat","tea"
        groups[key].append(w)        # no "if key in groups" needed
    return list(groups.values())
# Expected time O(n + sum(L * log(L + 1) for each word length L)).
# Keys can store O(total characters) references; output stores O(n) word references.

Follow-up. For strings containing only lowercase ASCII a–z, replace sorting with a 26-count tuple. Equal count tuples are equivalent to equal multisets under this alphabet contract. Empty strings are valid. Uppercase and other characters are outside the contract: some offsets are negative and can silently address the wrong slot rather than reliably raising an error. Validate or normalize first if external input may violate it. Expected time is O(n+total characters), including initializing 26 counters per word; keys and grouped references use O(n) storage for the fixed alphabet.

def group_anagrams_fast(words):
    groups = defaultdict(list)
    for w in words:
        count = [0] * 26
        for ch in w:
            count[ord(ch) - ord('a')] += 1
        groups[tuple(count)].append(w)   # tuple is hashable -> valid key
    return list(groups.values())

Decision: Sorted-string key vs count-tuple key for anagram grouping.

Name: Sorted string key

Gain: Two lines, obviously correct, trivial to explain.

Cost: O(k log k) per word to sort.

Pick: When words are short or clarity matters more than the last bit of speed.

Name: 26-count tuple key

Gain: O(k) per word — no sort; scales to long words.

Cost: More code; assumes a fixed alphabet (lowercase a-z).

Pick: When a reviewer asks you to remove the log factor, or words are long.

Name: Hand-rolled dict with guards

Gain: No imports.

Cost: Extra boilerplate and a place for bugs; slower to write.

Pick: Missing-key access must not insert automatically, imports are constrained, or explicit initialization makes the required behavior clearer.

Verdict: Use defaultdict when insertion on missing-key indexing is wanted. Choose a sorted key for arbitrary code-point strings, or a count tuple when its fixed alphabet is guaranteed. Measure before trading clarity for an unneeded optimization.

Where this shows up in production

collections.Counter and defaultdict are not design review-only toys — they are everywhere in real Python: tallying log lines by status code, grouping records by a key before aggregation, deduping with sets. heapq powers task schedulers and "keep the top N" dashboards; deque can hold a BFS frontier; it is bounded only when maxlen is supplied. A full bounded deque discards from the opposite end on append, so using maxlen for BFS without a proof can silently lose work. Fluency you build for design reviews is the same fluency that makes your day-to-day Python shorter and less buggy, because you stop hand-rolling primitives the standard library already got right.

You can now…

  • Count and group with Counter and defaultdict instead of manual guards.

  • Use heapq for top-K, including the negate-for-max-heap trick.

  • Reach for deque (not list.pop(0)) for BFS and window queues.

  • Write filter+map comprehensions and sorted(key=...) fluently.

  • Build strings with join while counting total output length and recognizing implementation-specific += optimizations.

  • Recite the CHeD-Sez peg and its canonical one-liners cold.

Self-check (answer follows each question).

Recall check

Q: You need the 3 largest elements. Two idiomatic ways?

A: heapq.nlargest(3, nums), or push negatives into a min-heap and negate on pop.

Q: Which collection do you use for a BFS queue and why not a plain list?

A: collections.deque — popleft() is O(1), whereas list.pop(0) is O(n) because elements shift.

Q: How do you build a frequency dict in one line, and how to get the two most common?

A: Counter(nums); then call .most_common(2) on that counter. No need to rebuild the counter twice.

Q: Why is ''.join(parts) preferred over building a string with += in a loop?

A: Recopying a growing prefix can yield quadratic work. CPython optimizes some += cases, but that is not a portable guarantee. Join costs O(number of pieces + total output characters) for an existing sequence of strings.

Q: Give the sort key to order pairs by count descending, then name ascending.

A: sorted(pairs, key=lambda p: (-p[1], p[0])), assuming numeric counts and mutually orderable names.

Q: ★ You wrote if key in d: d\[key].append(x) else: d\[key] = \[x]. Replace it with one construct and state why it is safer.

A: d = defaultdict(list) and d[key].append(x). This initializes on missing-key indexing; expected/amortized O(1) bookkeeping assumes bounded-cost hashing. It is convenient, not universally safer: .get() does not invoke the factory, and accidental indexing can create unwanted entries.

Practice queue

Practice both an explicit baseline and the relevant idiom. Explain changed behavior, not just reduced line count; the review-day labels are optional scheduling suggestions.

Next session (L04) puts the read-pointer / write-pointer skeleton to work on in-place array and string problems — the first real pattern, built on the vocabulary you just made automatic.

Corrective source notes

  • Python 3.14 collections — Counter missing/zero counts, defaultdict indexing versus get, and bounded deque discard behavior; passages rechecked 2026-10-09.
  • Python 3.14 heapq — min-heap operations and max-heap APIs introduced in 3.14; rechecked 2026-10-09. The examples use older portable min-heap APIs.
  • PEP 8 programming recommendations — do not rely on CPython’s fragile in-place string-concatenation optimization; checked 2026-10-09.

These citations support the named claims, not blanket certification of the lesson. Worked proofs are derived in the text; execution scope and finite-test limits are recorded separately.

Pause / Recall / Apply

Can you explain it without the page?

Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.

Stored in this browser only. No account, no sync. Clearing browser data removes your record.