Count Word Occurrences in a Python List

To count one word, use words.count("apple"). To count every word at once, use collections.Counter(words), which returns a dictionary of word to frequency and gives you the top results with .most_common(n).
The examples below were run on Python 3.13 (and pandas 3.0 for the last section). Everything except the pandas part uses only the standard library.
Counting a single word: list.count
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
print(words.count("apple"))
print(words.count("durian"))
3
0
list.count(x) scans the whole list and compares each element with ==. It is the simplest option when you need the frequency of one or two specific words. It returns 0 for missing items rather than raising.
Do not call it in a loop over every distinct word: each call is a full pass, so counting all k distinct words in a list of n items costs O(n × k). See the timing section below.
Counting every word: collections.Counter
Counter is a dict subclass built for exactly this job. It counts in a single pass.
from collections import Counter
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts)
print(counts["apple"], counts["durian"])
print(counts.most_common(2))
print(dict(counts))
Counter({'apple': 3, 'banana': 2, 'cherry': 1})
3 0
[('apple', 3), ('banana', 2)]
{'apple': 3, 'banana': 2, 'cherry': 1}
Useful properties:
- Looking up a missing key returns
0instead of raisingKeyError(and does not insert the key). most_common(n)returns thenhighest counts as(word, count)tuples, sorted descending. Ties keep first-seen order. With no argument it returns all items.- It is still a dictionary, so
.items(),.keys(),len()andjson.dumps(counts)all work.
Updating and combining counters
Counters support incremental updates and multiset arithmetic, which is handy when you process data in batches, files or days:
from collections import Counter
monday = Counter(["apple", "banana", "apple"])
tuesday = Counter(["banana", "cherry", "banana"])
total = monday + tuesday
print(total)
print(monday - tuesday) # drops zero/negative counts
print(monday & tuesday) # min of each count
print(monday | tuesday) # max of each count
monday.update(["cherry", "apple"])
print(monday)
monday.subtract(["apple"] * 5)
print(monday)
print(+monday) # strip non-positive
print(total.total()) # Python 3.10+
Counter({'banana': 3, 'apple': 2, 'cherry': 1})
Counter({'apple': 2})
Counter({'banana': 1})
Counter({'apple': 2, 'banana': 2, 'cherry': 1})
Counter({'apple': 3, 'banana': 1, 'cherry': 1})
Counter({'banana': 1, 'cherry': 1, 'apple': -2})
Counter({'banana': 1, 'cherry': 1})
6
Note the asymmetry: the - operator discards results that are zero or negative, while the subtract() method keeps them. update() adds counts, unlike dict.update(), which replaces values. total() sums all counts and was added in Python 3.10; on older versions use sum(counts.values()).
Counting with a plain dict or defaultdict
When you need custom logic inside the loop, or want to see what Counter does underneath, a manual loop is straightforward:
from collections import defaultdict
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = {}
for w in words:
counts[w] = counts.get(w, 0) + 1
print(counts)
dd = defaultdict(int)
for w in words:
dd[w] += 1
print(dict(dd))
print(sorted(counts.items(), key=lambda kv: kv[1], reverse=True))
{'apple': 3, 'banana': 2, 'cherry': 1}
{'apple': 3, 'banana': 2, 'cherry': 1}
[('apple', 3), ('banana', 2), ('cherry', 1)]
dict.get(key, 0) supplies a default for unseen words; defaultdict(int) does the same automatically because int() returns 0. Both are single-pass and O(n). Unlike Counter, reading a missing key from a defaultdict inserts it, which can quietly grow the dictionary.
Normalizing case and punctuation
Real-world words rarely arrive clean. "Apple", "apple," and "APPLE!" are three different strings, so they are counted separately unless you normalize first:
import string
from collections import Counter
words = ["Apple", "apple,", "APPLE!", "banana", "Banana."]
table = str.maketrans("", "", string.punctuation)
normalized = [w.translate(table).casefold() for w in words]
print(normalized)
print(Counter(normalized))
['apple', 'apple', 'apple', 'banana', 'banana']
Counter({'apple': 3, 'banana': 2})
str.casefold() is a more aggressive version of lower() intended for case-insensitive comparison (it maps German ß to ss, for instance). string.punctuation covers ASCII punctuation only; curly quotes, em dashes and other Unicode punctuation need a regex or an extended translation table.
Counting words in a block of text
Splitting text on whitespace leaves punctuation attached and keeps case differences:
import re
from collections import Counter
text = """The quick brown fox jumps over the lazy dog.
The dog barks; the FOX runs. Isn't it the fox's den?"""
naive = Counter(text.split())
print(naive["the"], naive["The"], naive["fox"])
tokens = re.findall(r"[a-z0-9]+(?:'[a-z]+)?", text.lower())
counts = Counter(tokens)
print(counts.most_common(4))
print(counts["fox"], counts["fox's"])
3 2 1
[('the', 5), ('fox', 2), ('dog', 2), ('quick', 1)]
2 1
Lowercasing first and extracting tokens with re.findall gives far more meaningful counts. The pattern keeps contractions and possessives (isn't, fox's) as single tokens; adjust it to your needs, for example r"\w+" for a looser match that also handles non-ASCII letters. For linguistic accuracy (stemming, lemmatization, multilingual tokenization) use a dedicated NLP library.
Ignoring stop words or counting specific words
from collections import Counter
STOP = {"the", "a", "an", "of", "and", "to", "is"}
words = "the cost of the fix is a fraction of the cost of the outage".split()
print(Counter(w for w in words if w not in STOP).most_common(3))
targets = ["cost", "fix", "bug"]
c = Counter(words)
print({t: c[t] for t in targets})
[('cost', 2), ('fix', 1), ('fraction', 1)]
{'cost': 2, 'fix': 1, 'bug': 0}
Counter accepts any iterable, so a generator expression filters without building an intermediate list. Counting once and then looking up several targets is better than calling list.count for each.
pandas: value_counts
If your words are already in a pandas Series or DataFrame column, use Series.value_counts():
import pandas as pd
s = pd.Series(["apple", "banana", "apple", "cherry", "banana", "apple"])
print(s.value_counts())
print(s.value_counts(normalize=True).round(3))
apple 3
banana 2
cherry 1
Name: count, dtype: int64
apple 0.500
banana 0.333
cherry 0.167
Name: proportion, dtype: float64
Results are sorted by frequency, normalize=True returns proportions, and dropna=False includes missing values. Since pandas 2.0 the resulting Series is named count (or proportion). Importing pandas just to count a list is overkill; reach for it when the data is already tabular.
Performance and complexity
Counting a list of 100,000 words drawn from a vocabulary of 1,000 (best of five runs with timeit on Python 3.13 on the build container for this article; numbers are indicative):
| Approach | Complexity | Time |
|---|---|---|
{w: words.count(w) for w in set(words)} | O(n × k) | 703 ms |
Counter(words) | O(n) | 2.2 ms |
defaultdict(int) loop | O(n) | 3.8 ms |
Counter is fastest because its counting loop for iterables is implemented in C. The list.count approach is hundreds of times slower here and gets worse as the vocabulary grows. Memory use is O(k) for the dictionary of distinct words. For text that does not fit in memory, feed Counter.update() line by line from the file.
Common pitfalls
- Counting characters by accident.
Counter("hello world")counts letters; split the string into words first. - Case and punctuation duplicates. Normalize with
casefold()and strip punctuation before counting. - Expecting
Counter.updateto overwrite. It adds; assigncounts[w] = nto set a value. - Negative counts after
subtract(). Use unary+countsto drop them. - Using
list.countin a loop. Fine for one word, quadratic for all of them.
Summary
Use list.count(word) for a one-off lookup and collections.Counter for everything else: it counts in one pass, returns 0 for unseen words, ranks results with most_common(), and supports update() and arithmetic for combining batches. Normalize case and punctuation before counting, tokenize raw text with re.findall, and use pandas value_counts() when the data already lives in a DataFrame.
PythonSplit a string into a list
Split strings into lists in Python: str.split with and without sep, maxsplit, rsplit, splitlines, re.split for multiple delimiters, list(), csv and shlex.
PythonNested dictionary values
Get values from nested dictionaries in Python: chained indexing, safe .get() chains, a deep_get helper, lists in dicts, recursion, json_normalize and glom.
PythonFactorial in Python
Compute factorials in Python with a loop, recursion, math.factorial, reduce and memoization, with input validation, big integers and real timeit benchmarks.