Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 32

Counting with `collections.Counter`

Recommended resources

Introduction

In the last lesson, we focused on selecting the right core container (list, tuple, set, dict) by analyzing their fundamental trade-offs in semantics and performance. This is the first layer of making Pythonic data structure choices.

Today, we'll build on that foundation by exploring the first of several specialized containers from Python's "batteries included" standard library: collections.Counter. This lesson directly addresses the learning outcome: Use collections.Counter to replace manual logic for counting hashable objects.

You'll learn how to replace verbose, manual counting loops—a common pattern for those coming from languages like Java—with a single, expressive, and efficient line of Python. This is a classic example of moving from a general-purpose tool (dict) to a specialized one that perfectly fits the problem.


The Manual Way: Counting with a Dictionary

A frequent programming task is to tally the occurrences of items in a sequence. For example, counting character frequencies in a string or tracking the number of times each word appears in a text.

Coming from a background where you might use a HashMap in Java, your first instinct in Python would be to use a dict. This involves iterating through the items and manually managing the counts.

Let's watch a video that demonstrates this evolution, starting with the most verbose, manual approach.

Idiomatic Python: Counting Things with Dictionaries

This video from MathByte Academy clearly shows the progression from a clunky, manual counting loop to more refined Pythonic solutions. We will follow this progression step-by-step.

First, watch from 00:53 to 02:46. This segment demonstrates the classic manual counting method using a for loop and an if/else check for key existence. Then, watch from 05:43 to 07:11, which shows an improvement using dict.get().

Let's break down the two approaches from the video.

Method 1: The "Look Before You Leap" (LBYL) approach

This is the most direct translation of the logic you might write in Java. You explicitly check if the key exists before you attempt to update its value.




# The "Bad" Way: Manual counting with if/else
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = {}

for word in words:
    if word in counts:
        counts[word] += 1
    else:
        counts[word] = 1

print(counts)



# Output: {'apple': 3, 'banana': 2, 'orange': 1}

This code is correct, but it's verbose. Python offers more expressive ways to handle this.

Method 2: A Better Way with dict.get()

As shown in the video, we can make this more concise by using the dict.get() method. get(key, default) returns the value for key if it's in the dictionary, otherwise it returns default. This eliminates the if/else block.




# A "Better" Way: Using dict.get()
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = {}

for word in words:
    counts[word] = counts.get(word, 0) + 1

print(counts)



# Output: {'apple': 3, 'banana': 2, 'orange': 1}

This is a common and useful Python idiom. However, for the specific task of counting, Python provides an even better, purpose-built tool.


The Pythonic Solution: collections.Counter

The "batteries-included" philosophy means that the standard library often has a specialized tool for common problems. For counting, that tool is collections.Counter. It's a dictionary subclass designed specifically for tallying hashable objects.

Let's see how Counter simplifies this task.

Python collections.Counter() vs. Manual Counting
This image clearly illustrates the verbosity of manual counting versus the concise, expressive power of `collections.Counter`.

Counter handles the entire loop and logic for you. You simply pass it an iterable (like a list or string), and it does the rest.




# The "Good" / Pythonic Way: Using collections.Counter
from collections import Counter

words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = Counter(words)

print(counts)



# Output: Counter({'apple': 3, 'banana': 2, 'orange': 1})

This single line of code is not only more readable and less error-prone, but as the video will show, it's also highly optimized and typically the fastest method.

Idiomatic Python: Counting Things with Dictionaries

Now, let's see the final, most Pythonic solution using Counter and review the performance comparison.

Watch from 08:57 to 13:30. This part introduces Counter as the ideal solution and then compares the execution time of all the methods discussed. Note how Counter is both the most readable and the most performant.


Mastering the Counter API

Now that you understand why Counter is the right tool, let's explore its powerful API. It behaves like a dictionary but adds several convenient features.

This article from Real Python provides an excellent, in-depth guide. We will use it as our primary reference for this section.

Python's Counter: The Pythonic Way to Count Objects

Please read the following sections from the Real Python article 'Python's Counter: The Pythonic Way to Count Objects'. This will walk you through creating and using Counter objects.

Read the following sections: 'Getting Started With Python’s Counter': Focus on the different ways to create a Counter (from iterables, dictionaries, and keyword arguments). 'Updating Object Counts': Note how .update() adds counts, unlike a regular dictionary's update. 'Accessing the Counter’s Content': Pay close attention to the fact that accessing a missing key returns 0 instead of a KeyError. 'Finding Most Common Objects': Learn about the extremely useful .most_common() method.

Let's supplement that reading with a video that demonstrates these API features in action.

Intermediate Python Tutorial #6 - Collections /Counter()

This video from Tech With Tim provides practical demonstrations of the most important Counter methods.

Watch the following segments to see the API in use: Creating a Counter (01:24 - 02:58): See how to initialize Counter from different sources. Accessing Counts (02:58 - 04:24): Notice the behavior with non-existent keys. .elements() and .most_common() (04:24 - 06:13): See how to get back the original items and find the most frequent ones. .update() and .subtract() (06:13 - 08:23): Observe how to modify counts. Arithmetic and Set Operations (08:51 - 11:37): Understand how to combine counters using operators like +, -, &, and |.


Practical Pattern: Counter with Generator Expressions

One of the most powerful ways to use Counter is by combining it with a generator expression. This allows you to perform a transformation on your data before it gets counted, all in one readable line.

Counter Comprehensions with Generator Expressions
This image shows two common patterns: counting an attribute of objects in a list, and cleaning up words before counting them.

Let's break down the second example from the image. Imagine you want to count words in a text but ignore punctuation and case.

The Manual Way:

text = "Hello, Python! What a wonderful world of Python."
words = []
for word in text.split():



    # Normalize the word before adding to list
    cleaned_word = word.strip(".,!").lower()
    words.append(cleaned_word)

counts = Counter(words)
print(counts)



# Output: Counter({'python': 2, 'hello': 1, 'what': 1, 'a': 1, 'wonderful': 1, 'world': 1, 'of': 1})

The Counter with Generator Expression Way:

text = "Hello, Python! What a wonderful world of Python."




# The generator expression `(word.strip(".,!").lower() for word in text.split())`
# processes each word on-the-fly as Counter consumes it.
counts = Counter(word.strip(".,!").lower() for word in text.split())

print(counts)



# Output: Counter({'python': 2, 'hello': 1, 'what': 1, 'a': 1, 'wonderful': 1, 'world': 1, 'of': 1})

This pattern is extremely common and is a hallmark of idiomatic Python code. It's concise, memory-efficient, and highly readable once you're familiar with the syntax.


Exercise: Refactor a Word Counter

Your task is to refactor a "bad" function that manually counts word frequencies into a "good" Pythonic function using collections.Counter.

The function should take a string of text, count the frequency of each word (case-insensitively, ignoring basic punctuation), and return the three most common words along with their counts.

from collections import Counter
import re

def get_top_3_words_manual(text: str) -> list[tuple[str, int]]:
    """
    A non-Pythonic function to find the top 3 most common words.
    - It's case-sensitive.
    - It doesn't handle punctuation well.
    - It manually sorts to find the top items.
    """
    counts = {}
    for word in text.split():



        # This is simple and flawed, we will improve it.
        word = word.lower() 
        if word in counts:
            counts[word] += 1
        else:
            counts[word] = 1
    



    # Manual sorting is complex and inefficient
    sorted_counts = sorted(counts.items(), key=lambda item: item[1], reverse=True)
    return sorted_counts[:3]


def get_top_3_words_pythonic(text: str) -> list[tuple[str, int]]:
    """
    A Pythonic function to find the top 3 most common words.
    - It should be case-insensitive.
    - It should strip punctuation from words.
    - It should use collections.Counter.
    """



    # Your implementation here.
    # Hint: Use a generator expression to clean the words before counting.
    # The regex re.findall(r'\b\w+\b', text.lower()) is a robust way to get words.
    words = re.findall(r'\b\w+\b', text.lower())
    word_counts = Counter(words)
    return word_counts.most_common(3)





# --- Verification ---
TEXT_SAMPLE = """
Python is a high-level, general-purpose programming language. Its design
philosophy emphasizes code readability with the use of significant indentation.
Python is dynamically typed and garbage-collected. It supports multiple
programming paradigms, including structured (particularly procedural),
object-oriented, and functional programming. Python is often described as a
'batteries included' language due to its comprehensive standard library.
"""




# Test the Pythonic function
top_3 = get_top_3_words_pythonic(TEXT_SAMPLE)
print(f"Top 3 words: {top_3}")




# Assertions to verify correctness
assert top_3[0][0] in ['python', 'is', 'a'] # Order can vary for same-count words
assert top_3[1][0] in ['python', 'is', 'a']
assert top_3[2][0] in ['python', 'is', 'a']
assert top_3[0][1] == 4 # 'python' appears 4 times
assert top_3[1][1] == 4 # 'is' appears 4 times
assert top_3[2][1] == 4 # 'a' appears 4 times




# Verify the next most common word is 'programming'
next_word, next_count = get_top_3_words_pythonic(TEXT_SAMPLE.replace("Python", ""))[2]
assert next_word == 'programming' and next_count == 2

print("\n✅ Exercise complete!")


Conclusion

In this lesson, you've seen how to move from a manual, verbose counting method to the specialized, efficient, and Pythonic collections.Counter. This embodies the principle of using the right tool for the job, a key aspect of the "batteries included" philosophy.

Key Takeaways:

  • Replace Manual Loops: Counter(iterable) is the direct replacement for manual for loops that use a dict to tally items.
  • Convenient API: Counter is a dict subclass, so it feels familiar, but it adds crucial features like returning 0 for missing keys and the .most_common() method.
  • Readability and Performance: Using Counter makes your code more declarative and is almost always faster than manual implementations.
  • Multiset Operations: Counter objects support arithmetic (+, -) and set-like (&, |) operations, making them powerful tools for comparing frequency distributions.

In our next lesson, we will continue exploring the collections module by looking at collections.defaultdict. You'll see how it elegantly solves the problem of grouping items into collections, another common task where manual if/else checks can be eliminated.