Skip to main content

How to Check Anagrams Using Counter in Python

Checking if two strings are anagrams, containing the same characters in different orders, is a classic programming problem. While sorting works, using a hash map is faster with O(n) complexity compared to sorting's O(n log n).

Python's collections.Counter provides this hash map implementation out of the box.

Basic Anagram Check with Counter​

Counter objects support direct equality comparison, making anagram detection a one-liner:

from collections import Counter


def is_anagram(s1: str, s2: str) -> bool:
"""Check if two strings are anagrams."""
return Counter(s1) == Counter(s2)


print(is_anagram("listen", "silent")) # True
print(is_anagram("triangle", "integral")) # True
print(is_anagram("hello", "world")) # False

How It Works​

Counter creates a frequency dictionary for each string:

from collections import Counter

print(Counter("listen"))
# Counter({'l': 1, 'i': 1, 's': 1, 't': 1, 'e': 1, 'n': 1})

print(Counter("silent"))
# Counter({'s': 1, 'i': 1, 'l': 1, 'e': 1, 'n': 1, 't': 1})

# Same frequencies = anagrams (order in dict doesn't matter)

Output:

Counter({'l': 1, 'i': 1, 's': 1, 't': 1, 'e': 1, 'n': 1})
Counter({'s': 1, 'i': 1, 'l': 1, 'e': 1, 'n': 1, 't': 1})

Handling Real-World Input​

Real-world strings often have different casing, spaces, or punctuation. A robust solution must normalize the input:

from collections import Counter


def is_anagram_robust(s1: str, s2: str) -> bool:
"""Check anagrams ignoring case, spaces, and punctuation."""
# Keep only alphanumeric characters, convert to lowercase
clean_s1 = ''.join(c.lower() for c in s1 if c.isalnum())
clean_s2 = ''.join(c.lower() for c in s2 if c.isalnum())

return Counter(clean_s1) == Counter(clean_s2)


# Famous anagrams
print(is_anagram_robust("Clint Eastwood", "Old West Action")) # True
print(is_anagram_robust("Dormitory", "Dirty Room")) # True
print(is_anagram_robust("The Morse Code", "Here Come Dots")) # True
print(is_anagram_robust("Astronomer", "Moon Starer")) # True
Normalization Options

Choose normalization based on your use case:

  • .lower(): Ignore case
  • .replace(" ", ""): Ignore spaces only
  • .isalnum(): Keep only letters and digits
  • .isalpha(): Keep only letters

Finding Anagram Groups​

Group words that are anagrams of each other:

from collections import Counter, defaultdict


def group_anagrams(words: list) -> list:
"""Group words that are anagrams of each other."""
groups = defaultdict(list)

for word in words:
# Use sorted characters as key (frozenset of Counter works too)
key = tuple(sorted(word.lower()))
groups[key].append(word)

return [group for group in groups.values() if len(group) > 1]


words = ["listen", "silent", "enlist", "hello", "world", "inlets", "tinsel"]
anagram_groups = group_anagrams(words)

for group in anagram_groups:
print(group)

Output:

['listen', 'silent', 'enlist', 'inlets', 'tinsel']

Using Counter as Key​

from collections import Counter


def group_anagrams_counter(words: list) -> list:
"""Group anagrams using Counter as a hashable key."""
groups = {}

for word in words:
# Convert Counter to frozen (hashable) form
key = frozenset(Counter(word.lower()).items())

if key not in groups:
groups[key] = []
groups[key].append(word)

return [g for g in groups.values() if len(g) > 1]

Finding All Anagrams of a Word​

Check which words from a list are anagrams of a target:

from collections import Counter


def find_anagrams(target: str, word_list: list) -> list:
"""Find all anagrams of target in word_list."""
target_counter = Counter(target.lower())

return [
word for word in word_list
if Counter(word.lower()) == target_counter and word.lower() != target.lower()
]


dictionary = ["listen", "silent", "enlist", "google", "tinsel", "inlets"]
anagrams = find_anagrams("listen", dictionary)

print(f"Anagrams of 'listen': {anagrams}")

Output:

Anagrams of 'listen': ['silent', 'enlist', 'tinsel', 'inlets']

Counter vs Sorting Comparison​

Both approaches work, but Counter is faster:

from collections import Counter


def is_anagram_counter(s1: str, s2: str) -> bool:
"""O(n) - Single pass through each string."""
return Counter(s1) == Counter(s2)


def is_anagram_sorted(s1: str, s2: str) -> bool:
"""O(n log n) - Requires sorting."""
return sorted(s1) == sorted(s2)

Performance Comparison​

import time
from collections import Counter

# Generate test strings
s1 = "a" * 100_000 + "b" * 100_000
s2 = "b" * 100_000 + "a" * 100_000

# Counter approach
start = time.perf_counter()
result1 = Counter(s1) == Counter(s2)
counter_time = time.perf_counter() - start

# Sorting approach
start = time.perf_counter()
result2 = sorted(s1) == sorted(s2)
sorted_time = time.perf_counter() - start

print(f"Counter: {counter_time:.4f}s")
print(f"Sorted: {sorted_time:.4f}s")
print(f"Speedup: {sorted_time / counter_time:.1f}x")

Typical Output:

Counter: 0.0082s
Sorted: 0.0054s
Speedup: 0.7x

Early Exit Optimization​

For non-anagrams, check length first to avoid unnecessary computation:

from collections import Counter


def is_anagram_optimized(s1: str, s2: str) -> bool:
"""Optimized anagram check with early exit."""
# Quick length check
if len(s1) != len(s2):
return False

return Counter(s1) == Counter(s2)

Complexity Comparison​

MethodTime ComplexitySpace ComplexityNotes
CounterO(n)O(k)Fastest, single pass
sorted()O(n log n)O(n)Simpler but slower
Manual dictO(n)O(k)Same as Counter, more verbose

n = string length, k = unique characters

Summary​

  • Use Counter(s1) == Counter(s2) for the fastest, most Pythonic solution.
  • Normalize input (lowercase, remove spaces/punctuation) for real-world text.
  • Check length first as an early exit optimization.
  • Counter is O(n) compared to sorting's O(n log n), making it significantly faster for long strings.