Python • Problem-Solving Patterns

Counting with dict/Counter — Use counting/frequency with dict/Counter

Master the art of tallying element frequencies to solve problems efficiently in Python.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

Historical Context & Motivation

Counting things is one of the oldest operations in computing. Long before modern programming languages existed, mathematicians and early computer scientists needed ways to tally frequencies — how many times each item appears in a dataset. From counting words in a book to analyzing letter frequencies for codebreaking during World War II, frequency analysis has been a fundamental tool. In Python, this pattern evolved from manual loops into elegant, built-in solutions.

1940s • Frequency Analysis in Codebreaking

Alan Turing and colleagues used letter frequency counting to crack the Enigma cipher, demonstrating the power of tallying occurrences in large datasets.

1991 • Python 1.0 — The dict Type

Guido van Rossum released Python with built-in dictionaries, giving programmers a hash-table-based structure perfect for counting items manually.

2003 • Python 2.3 — defaultdict Appears

The collections module introduced defaultdict, which eliminated the need to check if a key exists before incrementing a count.

2011 • Python 3.1 — Counter Arrives

The Counter class was added to the collections module, providing a purpose-built tool for counting hashable objects with handy methods like most_common().

Today • Universal Problem-Solving Pattern

Counting with dict and Counter is now one of the most common patterns in coding interviews, data analysis, and real-world Python applications.

The core question this lesson addresses is straightforward: given a collection of items, how do you efficiently determine how many times each unique item appears? While you could sort the data and scan through it, Python offers much faster and cleaner approaches using dictionaries and the Counter class.

SECTION 2

Core Principles & Definitions

Before diving into code, let's understand the foundational ideas behind frequency counting. A frequency map (also called a frequency table or histogram) is a mapping from each unique element to the number of times it appears. In Python, we build frequency maps using either a plain dict or the specialized collections.Counter class. Both rely on the same underlying principle: keys represent unique elements, and values represent their counts.

1

Frequency Map

A data structure that pairs each unique element with its count. In Python, this is a dictionary where keys are elements and values are integers representing how often each element appears.
2

Hashable Keys

Dictionary keys must be hashable — meaning they have a fixed hash value. Strings, numbers, and tuples are hashable; lists and other dicts are not. This is what makes lookups run in O(1) average time.
3

The dict Approach

Using a plain dictionary, you iterate through a collection and manually increment counts. You use dict.get(key, 0) or check membership with 'in' to handle keys that don't exist yet.
4

The Counter Class

Counter is a dict subclass from the collections module. You pass it any iterable, and it automatically builds the frequency map. It also provides methods like most_common() and supports arithmetic between Counters.
5

O(n) Time Complexity

Both approaches scan the input once, making them O(n) in time where n is the number of elements. Each lookup and insertion in a dict averages O(1), so the total work scales linearly.
✦

KEY TAKEAWAY

Think of a frequency map like a teacher taking attendance. As each student walks in, the teacher puts a tally mark next to their name. At the end, you know exactly how many times each name was called. A Python dict is like doing it by hand; Counter is like having a smart clipboard that tallies automatically.

SECTION 3

Visual Explanation — How Counting Works

The diagram below traces how a frequency map is built step by step from a list of fruits. Each time we encounter an element, we either create a new entry with a count of 1 or increment an existing entry. Watch how the dictionary grows as we move through the input.

Building a Frequency Map: Step-by-StepInput list:applebananaapplecherrybananaappleStepCurrent ElementDict State After Step1apple{apple: 1}2banana{apple: 1, banana: 1}3apple{apple: 2, banana: 1}4cherry{apple: 2, banana: 1, cherry: 1}5banana{apple: 2, banana: 2, cherry: 1}6apple{apple: 3, banana: 2, cherry: 1}Final Result: {"apple": 3, "banana": 2, "cherry": 1}Equivalent to Counter(["apple","banana","apple","cherry","banana","apple"])
Each row shows how the dictionary state changes as we process one element at a time. When an element already exists (highlighted rows), its count is incremented rather than a new key being created.

Notice how at steps 3, 5, and 6, the element already exists in the dictionary. Instead of adding a new key, we simply increment the existing value. This is the core logic behind both the manual dict approach and the Counter class — the difference is that Counter handles all of this automatically when you pass it an iterable.

SECTION 4

How It Works — Code Patterns

Pattern 1: Manual Counting with dict

The most basic pattern uses a plain dictionary and the get method. For each element in your iterable, you call counts.get(item, 0) + 1. The second argument to get() is a default value that's returned when the key doesn't exist yet. This avoids a KeyError and makes the code clean.

DICT.GET PATTERN

counts=foritemindata:counts[item]=counts.get(item,0)+1counts = {} for item in data: counts[item] = counts.get(item, 0) + 1

counts — the frequency dictionary; data — the input iterable; item — current element; get(item, 0) returns the current count or 0 if the item hasn't been seen yet.

Pattern 2: Using if/else

An alternative manual approach checks membership with the in keyword. If the key already exists, increment it; otherwise, set it to 1. This pattern is more verbose but can be easier to read when you're first learning.

IF/ELSE PATTERN

counts=foritemindata:ifitemincounts:counts[item]+=1else:counts[item]=1counts = {} for item in data: if item in counts: counts[item] += 1 else: counts[item] = 1

item in counts checks whether the key already exists. This runs in O(1) average time thanks to hashing.

Pattern 3: collections.Counter

The Counter class from the collections module does all the counting in a single line. You just pass it any iterable — a list, string, tuple, or even a generator — and it returns a Counter object (which behaves like a dictionary). It also comes with useful methods like most_common(n) which returns the n most frequent elements as a list of tuples.

COUNTER PATTERN

from collections import Counter counts = Counter(data) # counts.most_common(2) → top 2 elements # counts[item] → count of item (0 if missing)

Counter automatically handles missing keys by returning 0 instead of raising a KeyError, making it safer and more convenient than a plain dict.

💡

Counter vs. dict — When Missing Keys Are Accessed

If you access a missing key on a plain dict, Python raises a KeyError. But a Counter returns 0 for any missing key. This means Counter('hello')['z'] gives 0 — no error, no crash. This behavior is extremely useful in many problem-solving scenarios.

SECTION 5

Common Use Cases & Classification

Frequency counting shows up in a surprisingly wide range of problems. Once you learn to recognize when a problem is really asking 'how many of each?', you'll find that dict and Counter simplify your solutions dramatically. Below is a visual classification of the most common use cases you'll encounter in coding exercises and real applications.

Common Frequency Counting Use CasesFrequency CountingString ProblemsArray / List ProblemsData AnalysisAnagram detectionMost common characterUnique char countPangram checkerFind duplicatesMajority elementTop-K frequent itemsElement intersectionWord frequency in textVote tallyingInventory trackingSurvey responsesThe Pattern: "How many of each?"Whenever a problem asks about frequency, duplicates, most common,or grouping by count — reach for dict or Counter.
Frequency counting problems fall into three main categories: string-based problems like anagram detection, array/list problems like finding duplicates, and data analysis tasks like tallying votes or tracking inventory.

The key insight is to look for trigger words in problem descriptions. Phrases like "how many times", "most frequent", "duplicate", and "appears more than" all signal that frequency counting is the right approach. When you spot these cues, your first instinct should be to build a frequency map.

SECTION 6

Worked Example — Finding the Most Frequent Character

Let's walk through a complete problem: given a string, find the character that appears most often. If there's a tie, return the one that comes first alphabetically. We'll solve it two ways — first with a plain dict, then with Counter.

Most Frequent Character (dict approach)

  1. 1

    Step 1 — Define the Input

    Our input string is s = "banana". We need to count each character's frequency and then find the maximum.
  2. 2

    Step 2 — Build the Frequency Map

    We create an empty dictionary and loop through each character. Using counts[char] = counts.get(char, 0) + 1, we build the map.
    counts = {'b': 1, 'a': 3, 'n': 2}
  3. 3

    Step 3 — Find the Maximum Count

    We use max_count = max(counts.values()) to find the highest frequency.
    max_count = 3
  4. 4

    Step 4 — Handle Ties (Alphabetical Tiebreaker)

    We collect all characters with the maximum count: candidates = [ch for ch, cnt in counts.items() if cnt == max_count]. Then we return min(candidates) to pick the alphabetically first one.
    candidates = ['a'], so the answer is 'a'
  5. 5

    Step 5 — Counter Shortcut

    With Counter, this simplifies to: from collections import Counter; counts = Counter(s); print(counts.most_common(1)). The most_common(1) method returns [('a', 3)], giving us both the character and its count in one call.
    Most frequent character: 'a' with 3 occurrences.
ℹ

Full Code — Both Versions

Dict version: counts = {}; [counts.__setitem__(c, counts.get(c,0)+1) for c in s] — but use a regular for-loop for clarity. Counter version: Counter(s).most_common(1)[0][0] — a single expression that returns the answer directly.

SECTION 7

dict vs. Counter — Strengths & Limitations

Both approaches accomplish the same goal, but they each have trade-offs. Understanding when to use a plain dict versus Counter will make you a more effective problem solver. The table below breaks down the key differences.

FeaturePlain dictCounter
Import needed?No — built-inYes — from collections
Lines of code to count3–5 lines (loop required)1 line (pass iterable)
Missing key behaviorRaises KeyErrorReturns 0
most_common() methodNot availableBuilt-in, returns sorted list
Arithmetic (+, −)Manual merge neededSupports +, −, &, | operators
FlexibilityFull control over logicLess customizable
PerformanceO(n) — sameO(n) — same (slight overhead)

Comparison of dict and Counter for frequency counting

🔑

WHEN TO USE WHICH

Use a plain dict when you need custom logic during counting (like filtering elements or doing extra processing in the loop). Use Counter when you want clean, readable code and need features like most_common() or Counter arithmetic. In coding interviews, Counter is usually preferred because it saves time and shows you know Python's standard library.

SECTION 8

Connection to Advanced Techniques

Frequency counting is a foundational skill that connects to many more advanced problem-solving techniques. Once you're comfortable with dict and Counter, you'll find them appearing inside larger algorithms. Here's how the basic pattern scales up.

Basic ConceptAdvanced Extension
Count characters in a stringSliding window frequency maps — track counts in a moving window to solve substring problems
Find the most common elementTop-K problems using Counter.most_common(k) combined with heaps for large datasets
Check if two strings are anagramsGroup anagrams — use sorted-string or Counter as dictionary keys to cluster words
Count duplicates in a listBucket sort — use frequency as an index to sort in O(n) time without comparisons
Simple frequency mapdefaultdict(list) for grouping — values become lists of items instead of counts

How basic counting connects to advanced patterns

The sliding window technique is particularly important. It combines frequency counting with two pointers to solve problems like "find the longest substring with at most K distinct characters." You maintain a Counter as your window expands and contracts, adding and removing elements. This runs in O(n) time and is a staple of coding interviews at every level.

ℹ

Looking Ahead

As you advance, you'll also encounter defaultdict(int) from the collections module. It works like a dict but automatically initializes missing keys to 0 (or any default you specify). It's the middle ground between a plain dict and Counter — more flexible than Counter but less boilerplate than a plain dict.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why a Python dictionary is a good data structure for counting frequencies. What property of dictionaries makes lookups fast?
Show answer
A Python dictionary uses a hash table internally, which means looking up whether a key exists and retrieving its value both run in O(1) average time. This is ideal for frequency counting because for each element in our input, we need to quickly check if it already has a count and then update it. If we used a list instead, searching for an element would take O(n) time, making the entire counting process O(n²). The key-value structure of dicts naturally maps each unique element (key) to its count (value), making it a perfect fit for frequency maps.

Question 2: BASIC CALCULATION

Given the list nums = [4, 1, 2, 1, 4, 4, 3, 2, 1], write code using a plain dict (with the .get() method) to build a frequency map. What is the resulting dictionary?
Show answer
The code is: counts = {}; for n in nums: counts[n] = counts.get(n, 0) + 1. Walking through the list: 4 appears 3 times, 1 appears 3 times, 2 appears 2 times, and 3 appears 1 time. The resulting dictionary is {4: 3, 1: 3, 2: 2, 3: 1}. Note that the key order reflects the order each element was first encountered. You could verify this by running Counter(nums) which produces the same counts.

Question 3: INTERMEDIATE

Write a function are_anagrams(s1, s2) that returns True if two strings are anagrams of each other (contain exactly the same characters with the same frequencies). Use Counter.
Show answer
The solution is elegant: from collections import Counter; def are_anagrams(s1, s2): return Counter(s1) == Counter(s2). Two Counter objects can be compared with == directly, and they're equal if and only if every key has the same count in both. For example, Counter('listen') and Counter('silent') both produce {'l':1, 'i':1, 's':1, 't':1, 'e':1, 'n':1}, so they're equal. This runs in O(n) time where n is the length of the longer string, compared to O(n log n) if you sorted both strings. You could also solve this with a plain dict, but you'd need to build two frequency maps and then compare them key by key.

Question 4: APPLIED

A teacher has a list of test scores: scores = [85, 92, 78, 92, 85, 100, 78, 85, 92, 78, 100, 85]. Write code using Counter to: (a) find the most common score, (b) determine how many unique scores there are, and (c) list all scores that appear more than 2 times.
Show answer
Solution: from collections import Counter; c = Counter(scores). (a) c.most_common(1) returns [(85, 4)], so 85 is the most common score with 4 occurrences. (b) len(c) returns 4, since there are 4 unique scores (78, 85, 92, 100). (c) [score for score, count in c.items() if count > 2] returns [85, 92, 78] because 85 appears 4 times, 92 appears 3 times, and 78 appears 3 times. The score 100 only appears twice, so it's excluded. This is a realistic scenario where Counter makes data analysis straightforward.

Question 5: CRITICAL THINKING

The "majority element" problem asks you to find an element that appears more than n/2 times in a list of length n (it's guaranteed to exist). You could solve this with Counter, but can you think of why Counter might not be the most memory-efficient approach? Describe both the Counter solution and suggest what additional optimization might be possible.
Show answer
The Counter solution is simple: Counter(nums).most_common(1)[0][0]. It works in O(n) time but uses O(n) extra space to store the entire frequency map. However, since the majority element appears more than n/2 times, we can solve it with O(1) extra space using Boyer-Moore Voting Algorithm: maintain a candidate and a count, incrementing when you see the candidate and decrementing otherwise. When the count hits 0, switch candidates. The majority element will always be the final candidate. This shows that while Counter is an excellent general-purpose tool, understanding the specific properties of a problem can lead to more efficient solutions. In an interview, mentioning both approaches demonstrates depth of thinking — start with the simple Counter solution, then optimize if needed.

SUMMARY

Lesson Summary

Summary

Frequency counting is one of the most versatile patterns in Python programming. You can build a frequency map using a plain dict with the .get() method or an if/else check, or you can use collections.Counter for a one-line solution that also provides powerful methods like most_common() and arithmetic operators. Both approaches run in O(n) time thanks to hash-table-based O(1) lookups.

Look for trigger words in problems — "how many times," "most frequent," "duplicates," and "appears more than" — to know when to apply this pattern. Common applications include anagram detection, finding duplicates, top-K problems, and data analysis tasks. As you advance, this pattern becomes the foundation for techniques like the sliding window and bucket sort.

Varsity Tutors • Python • Counting with dict/Counter