Counting repeated values looks simple until the surrounding code starts accumulating special cases. A plain dictionary can tally events, words, status codes, or inventory units, but the implementation also has to initialize missing keys, rank frequent values, merge counts, and decide what zero or negative counts mean.
Python’s collections.Counter packages those operations into a dictionary-like type designed for counting hashable objects. It is useful when the problem is fundamentally about frequencies or multisets rather than arbitrary key-value storage.
The important part is not merely replacing a few lines of dictionary code. Counter has semantics that differ from a normal dictionary, especially around missing keys, updates, subtraction, and multiset arithmetic. Understanding those rules keeps concise code from becoming surprising code.
Count an iterable directly
A Counter can consume an iterable and count each element:
from collections import Counter
events = ["ok", "timeout", "ok", "error", "ok", "timeout"]
counts = Counter(events)
print(counts)The logical counts are:
ok: 3
timeout: 2
error: 1The elements must be hashable because they become dictionary keys. Strings, integers, tuples containing hashable values, and many immutable application identifiers work naturally. Lists and other unhashable values cannot be counted directly as keys.
For structured records, count the field that represents the category:
from collections import Counter
requests = [
{"path": "/health", "status": 200},
{"path": "/items", "status": 200},
{"path": "/items", "status": 404},
]
status_counts = Counter(request["status"] for request in requests)
print(status_counts[200])This keeps extraction separate from counting and avoids building an intermediate list.
Missing keys return zero
A normal dictionary raises KeyError for a missing key unless code uses a helper such as get(). A Counter returns zero:
from collections import Counter
counts = Counter({"success": 8})
print(counts["success"])
print(counts["failure"])The second lookup returns 0.
That behavior makes incremental counting straightforward:
counts = Counter()
for status in ["ok", "ok", "error"]:
counts[status] += 1No explicit initialization is required.
Zero is not the same as absence
Assigning zero does not delete a key:
counts = Counter({"ok": 2})
counts["error"] = 0
print("error" in counts)The result is True.
Use del when the entry itself should disappear:
del counts["error"]This distinction matters when iterating over keys, serializing the mapping, or measuring the number of stored entries. A zero count behaves like zero for counting purposes, but the key can still be present in the underlying mapping.
Find the most common values
most_common() returns elements and counts ordered from the highest count downward:
from collections import Counter
counts = Counter("mississippi")
print(counts.most_common(2))This is convenient for frequency reports and top-N summaries.
If two elements have equal counts, their order follows the order in which they were first encountered. That gives deterministic behavior for a single known input order, but it should not be mistaken for an alphabetical or numeric tie-breaker.
If the application requires a different tie rule, express it explicitly:
items = sorted(counts.items(), key=lambda item: (-item[1], item[0]))Here the primary key is descending frequency and the secondary key is ascending element value.
Add counts instead of replacing them
Counter.update() differs from dict.update(). It adds counts rather than replacing existing values:
from collections import Counter
counts = Counter({"ok": 3, "error": 1})
counts.update(["ok", "timeout", "ok"])
print(counts)The resulting counts are equivalent to:
ok: 5
error: 1
timeout: 1When update() receives an iterable, that iterable should contain elements to count. It is not interpreted as an iterable of (key, value) pairs in the way some dictionary-building APIs are.
To add explicit quantities, pass a mapping or another Counter:
counts.update({"ok": 4, "error": 2})This adds four to ok and two to error.
Subtract without losing negative results
The subtract() method decrements counts and keeps zero or negative values:
from collections import Counter
inventory = Counter({"widget": 4, "cable": 2})
inventory.subtract({"widget": 1, "cable": 3})
print(inventory)The logical result is:
widget: 3
cable: -1Keeping negative values is useful when a counter represents a signed balance, discrepancy, or running delta.
It also means Counter does not enforce inventory validity. If negative stock is forbidden in your domain, validate before or after applying the change rather than assuming the container will reject it.
Counter subtraction has different semantics
The binary - operator is not identical to subtract().
from collections import Counter
available = Counter({"widget": 4, "cable": 2})
used = Counter({"widget": 1, "cable": 3})
remaining = available - used
print(remaining)Multiset subtraction keeps only positive output counts. The negative cable result is omitted.
Use subtract() when signed differences matter. Use - when you want multiset-style subtraction whose result contains only positive multiplicities.
Normalize away non-positive counts
Unary plus is a concise way to create a new counter containing only positive counts:
from collections import Counter
balance = Counter({"ready": 3, "pending": 0, "failed": -2})
active = +balance
print(active)active contains only ready: 3.
This is useful after signed updates when downstream code needs an ordinary positive multiset.
Do not use this transformation if zero or negative entries carry domain meaning. Cleaning a counter changes information, so it should happen at a deliberate boundary rather than automatically after every operation.
Combine counters as multisets
Counters support arithmetic that is useful for positive-count multisets.
Addition sums corresponding counts:
from collections import Counter
warehouse_a = Counter({"bolt": 4, "nut": 2})
warehouse_b = Counter({"bolt": 1, "washer": 3})
combined = warehouse_a + warehouse_bIntersection with & keeps the minimum corresponding positive count:
common = warehouse_a & warehouse_bUnion with | keeps the maximum corresponding positive count:
maximums = warehouse_a | warehouse_bThese are multiset operations, not set operations on the keys alone. A count represents how many copies of an element participate.
The arithmetic result excludes counts that are zero or negative. If your application treats signed counts as first-class data, ordinary per-key arithmetic may communicate the intent better than multiset operators.
Compare required and available quantities
Counter comparisons can express multiset containment directly:
from collections import Counter
required = Counter({"bolt": 4, "nut": 4})
available = Counter({"bolt": 6, "nut": 4, "washer": 2})
if required <= available:
print("requirements can be satisfied")The comparison checks corresponding counts, treating missing elements as having a count of zero.
This is compact for requirements, recipes, resource bundles, or test expectations where each key represents a quantity.
Be careful with signed counters. Multiset containment is easiest to reason about when counts represent non-negative quantities. If negative values mean debt, reservations, or adjustments, define the business rule explicitly rather than assuming mathematical containment matches it.
Reconstruct repeated elements with elements()
elements() returns an iterator that repeats each element according to its count:
from collections import Counter
parts = Counter({"bolt": 3, "nut": 2})
print(list(parts.elements()))The result contains three bolt values and two nut values.
Counts less than one are ignored. The method is therefore suitable for expanding a positive multiset, not for preserving signed balance information.
Because expansion produces one output per counted occurrence, it can also be expensive when counts are large. If a counter says an event occurred ten million times, iterating over elements() means producing ten million values. Keep the compact count representation when individual repetitions are not actually needed.
Sum counts carefully across Python versions
Modern Python versions provide Counter.total() to sum the stored counts:
from collections import Counter
counts = Counter({"ok": 8, "error": 2})
print(counts.total())total() was added in Python 3.10. If a library must support older Python versions, the portable equivalent is:
total = sum(counts.values())Both approaches include negative and zero counts because they sum the stored values rather than counting only positive elements.
Do not confuse this with len(counts). len() reports the number of stored keys, including keys whose counts are zero or negative.
Use a Counter for streaming tallies
A counter does not require all input values to exist at once. It can accumulate batches:
from collections import Counter
def count_batches(batches):
counts = Counter()
for batch in batches:
counts.update(batch)
return countsThis is useful when the input arrives from files, paginated APIs, queues, or other iterators.
The memory benefit has a limit: the counter stores one entry for each distinct key it has encountered. A stream with a small fixed vocabulary stays compact, while a stream containing millions of unique identifiers can still consume substantial memory.
For unbounded-cardinality telemetry, approximate frequency algorithms or external aggregation systems may be more appropriate. Counter is exact, so exact distinct keys have to be represented somewhere.
Keep normalization outside the Counter
Frequency analysis often needs normalization before counting. For example, text may need case folding:
from collections import Counter
words = ["Error", "error", "TIMEOUT", "timeout"]
counts = Counter(word.casefold() for word in words)The counter should usually receive the identity you actually intend to count.
Do not hide unrelated normalization rules inside a custom Counter subclass unless the domain strongly requires it. Explicit preprocessing makes it easier to answer questions such as whether "Error" and "error" are intentionally the same category.
The same principle applies to paths, status labels, user-entered tags, and other values whose canonical form may need careful definition.
Do not use Counter when a plain dictionary says more
Counter is specialized. A normal dictionary is often clearer when values are not conceptually counts.
For example, this is technically possible:
from collections import Counter
settings = Counter()
settings["timeout"] = 30But timeout is a configuration value, not a multiplicity. Missing-key-as-zero behavior and multiset operators would be misleading.
Choose Counter when addition, subtraction, frequency ranking, or multiset reasoning naturally describes the problem. Choose a normal dictionary for arbitrary attributes and mappings.
Common pitfalls
Assuming zero-count keys disappear
They remain stored until deleted or removed by an operation that constructs a positive-only multiset.
Expecting update() to replace values
For counters, update() adds counts. Direct assignment replaces a specific count.
Treating subtract() and the - operator as equivalent
subtract() preserves zero and negative results. Multiset subtraction discards non-positive output counts.
Expanding huge counts with elements()
The iterator repeats each positive-count element. Large multiplicities can produce very large outputs.
Relying on top-N tie order as a business rule
most_common() uses first-encounter order for equal counts. Add an explicit secondary sort key when the domain requires another tie-breaker.
Forgetting cardinality growth
A counter is compact relative to storing every occurrence, but it still grows with the number of distinct keys.
Model counts, not just mappings
Counter is most useful when the program treats values as quantities rather than generic dictionary data.
Use it to tally hashable values, merge batches, report frequent items, compare required quantities, or perform multiset arithmetic. Preserve signed counts when they carry meaning, and deliberately normalize to positive counts only when the next stage expects a multiset.
The resulting code is often shorter than a hand-built dictionary solution, but the larger benefit is semantic: operations such as “add these observations,” “subtract these quantities,” and “find the most common values” become explicit in the data structure itself.