Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsbeginner 60m read

L03 · Python for Speed — The 20 Idioms That Matter

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

🧩DSAPhase 0 · Foundations· Session 003 of 130 60 min

🎯 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 Interview-Ready · Session 3 / 65 · Phase 0 · Foundations

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 interview problem. 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 interview minutes. The idioms below collapse those to one line each.

The analogy
🌍 Real world
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.
💻 Code world
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
 
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

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)
 
heapq.heappush(h, -x)        # push NEGATIVES to simulate a MAX-heap
-heapq.heappop(h)            # negate again on the way out
 
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

Comprehensions and iteration helpers

[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

The little sharp tools

"".join(parts)               # build a string in O(n), NOT += in a loop (O(n^2))
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
From first principles
Start with the question
Why memorise idioms instead of just writing the logic out longhand each time?
  1. 1
    An interview gives you ~20 minutes of thinking, not typing.
    forced by · The clock does not pause while you recall dict-default syntax.
  2. 2
    Boilerplate (manual counting, guards) is where silent bugs hide.
    forced by · An off-by-one in a hand-rolled counter is easy to write and hard to spot under stress.
  3. 3
    Standard-library tools are already correct and complexity-optimal.
    forced by · Counter, heapq, deque are C-implemented and battle-tested.
  4. 4
    Fluency frees working memory for the algorithm.
    forced by · If typing is automatic, your attention stays on the invariant and edge cases.
⇒ Therefore
A small automatic vocabulary converts routine sub-tasks into reflexes, so the scarce resource — thinking time — goes to the part that is actually hard.

The template — a solution skeleton using the vocabulary

Most array/string problems fit this shape. Notice how little of it is bespoke:

def solve(nums, target):
    from collections import Counter, defaultdict
    import heapq
 
    counts = Counter(nums)              # 1. summarise the input
    groups = defaultdict(list)          # 2. group if needed
    for i, x in enumerate(nums):        # 3. one pass with index+value
        groups[some_key(x)].append(i)
 
    heap = []                           # 4. if you need "top/smallest K"
    for key, val in counts.items():
        heapq.heappush(heap, (val, key))
        if len(heap) > k:               # keep heap size at k -> O(n log k)
            heapq.heappop(heap)
 
    return [key for val, key in heap]   # 5. comprehension to shape the output

You will not use every line every time. The value is that each line is a reflex, so you assemble the right subset in seconds.

Mental modelSummarise, group, prioritise, shape
Four reflexes, in order: (1) Counter to summarise, (2) defaultdict to group, (3) heapq to prioritise a top/bottom K, (4) a comprehension to shape the answer. Most solutions are a subset of these plus the real algorithm in the middle.
  • 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 output -> comprehension or ''.join, never += in a loop.
🔔 Fires when you see
Whenever you catch yourself writing an 'if key in d' guard or a manual max-tracking loop, stop — there is an idiom for that.

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.

Common misconception
✗ What most people think
`heapq` gives me a max-heap if I want the largest elements.
✓ What is actually true
Python's heapq is ALWAYS a min-heap. For a max-heap, push the negatives of your values and negate again when you pop.
Why the myth is so sticky
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.
Prove it to yourself
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 interviewers 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) · Amazon, Bloomberg (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) · Amazon, Uber (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) · Amazon, Meta (Medium) — probes: Counter + heapq.nlargest (O(n log k)) or bucket sort (O(n)). Follow-up: "can you do it in O(n)?"
  • K Closest Points to Origin (973) · Amazon (Medium) — probes: a size-K max-heap of negative distances. Follow-up: "streaming points, unknown total."
  • Number of Islands (200) · Amazon, Microsoft, Meta (Medium) — probes: deque for BFS flood fill (or recursion for DFS). Follow-up: "very large grid → BFS to avoid recursion limits."
  • Task Scheduler (621) · Amazon, Meta (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: Return the k most frequent elements of nums.

Brute + cost. Count into a dict, sort all pairs by count, take the top k: O(n log n) from the full sort. Fine, but we can do O(n log k) with a bounded heap, and O(n) with buckets.

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]
# Time O(n log k), space O(n).

Even better (bucket sort). Counts range from 1..n, so index buckets by count:

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).

Complexity. Heap: O(n log k). Bucket: O(n). Both use the vocabulary (Counter, heapq) rather than reinventing counting.

Follow-up. "Ties broken by value" → make the heap entries (count, value) and rely on tuple ordering, or sort within a bucket.

Try itProve the max-heap negation trick.
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.

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

Worked example 2 — Group Anagrams with defaultdict

Problem in plain words: Group words that are anagrams of each other.

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())
# Time O(n * k log k) for n words of length k; space O(n*k).

Follow-up. "Drop the sort from the key." → use a 26-length count tuple as the key: O(n·k) total, no per-word sort.

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())
The tradeoff
Sorted-string key vs count-tuple key for anagram grouping.
Sorted string key
+ you gain Two lines, obviously correct, trivial to explain.
− you pay O(k log k) per word to sort.
pick when When words are short or clarity matters more than the last bit of speed.
26-count tuple key
+ you gain O(k) per word — no sort; scales to long words.
− you pay More code; assumes a fixed alphabet (lowercase a-z).
pick when When the interviewer asks you to remove the log factor, or words are long.
Hand-rolled dict with guards
+ you gain No imports.
− you pay Extra boilerplate and a place for bugs; slower to write.
pick when Never in practice — defaultdict is strictly better.
What a senior engineer actually does
Reach for defaultdict every time; start with the sorted-string key for clarity and switch to the count tuple the moment speed is challenged.

Where this shows up in production

collections.Counter and defaultdict are not interview-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 is the standard bounded buffer and BFS frontier. Fluency you build for interviews 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, never += in a loop.
  • Recite the CHeD-Sez peg and its canonical one-liners cold.
Recall check · click to reveal
★ = stretch question

Practice queue

Solve each using the idioms — no hand-rolled counters or guards.