The str.count(sub[, start[, end]]) method returns the integer number of non-overlapping occurrences of substring sub in the range [start:end].
The Non-Overlapping Invariant: Python scans strictly left-to-right and consumes matched characters, meaning overlapping patterns are not counted twice. For example, "aaaa".count("aa") returns 2, not 3. If the target substring is absent, count() returns 0 without raising an exception.
The standard CPython signature for str.count() is:
str.count(sub, start=0, end=sys.maxsize, /)
| Parameter | Type | Default | Description |
|---|---|---|---|
sub |
str |
Required | The target substring to search for. Must be a string. |
start |
int |
0 |
Optional. 0-based beginning index (inclusive). Supports negative indices counting backward from the end. |
end |
int |
End of string | Optional. 0-based termination index (exclusive). Supports negative indices. |
int ≥ 0). If sub does not appear within the evaluated range, 0 is returned.
Counting occurrences of single characters or multi-character phrases:
text = "Welcome to plus2net.com. Enjoy learning Python at plus2net!"
# Count substring occurrences
site_count = text.count("plus2net")
letter_o_count = text.count("o")
print("Occurrences of 'plus2net':", site_count)
print("Occurrences of 'o': ", letter_o_count)
Expected Output:
Occurrences of 'plus2net': 2
Occurrences of 'o': 5
Notice that count() is strictly case-sensitive: searching for "python" in "Python" yields 0.
The most important algorithmic rule of str.count() is that matches never overlap. Once Python matches an occurrence, it advances its internal cursor to the character immediately following the match:
sequence = "aaaa"
# Non-overlapping count: matches indices [0:2] and [2:4]
count_aa = sequence.count("aa")
print("'aaaa'.count('aa'):", count_aa) # Output: 2
# 'banana' contains two overlapping 'ana' sequences: index 1 and index 3
fruit = "banana"
print("'banana'.count('ana'):", fruit.count("ana")) # Output: 1
Expected Output:
'aaaa'.count('aa'): 2
'banana'.count('ana'): 1
import re; len(re.findall(r'(?=(ana))', 'banana')) → 2
You can constrain the count to a specific substring window without creating an intermediate slice string:
phrase = "ab_ab_ab_ab"
# Entire string
print("Full count: ", phrase.count("ab"))
# From index 3 to the end
print("From index 3: ", phrase.count("ab", 3))
# Range [3:8]
print("Range [3:8]: ", phrase.count("ab", 3, 8))
# Negative indices: last 5 characters
print("Last 5 characters: ", phrase.count("ab", -5))
Expected Output:
Full count: 4
From index 3: 3
Range [3:8]: 2
Last 5 characters: 2
Passing phrase.count("ab", 3, 8) is functionally equivalent to phrase[3:8].count("ab"), but avoids allocating memory for the intermediate substring slice.
An interesting mathematical edge case in CPython is counting the empty string "":
word = "code"
print("Length: ", len(word))
print("count(''): ", word.count(""))
print("''.count(''): ", "".count(""))
Expected Output:
Length: 4
count(''): 5
''.count(''): 1
For any string of length N, the empty string occurs at N + 1 positions (before each character, between every adjacent pair, and after the final character).
Calculating Guanine-Cytosine content in DNA genomic sequences:
def calculate_gc_content(dna_sequence: str) -> float:
seq_upper = dna_sequence.upper()
total_bases = len(seq_upper)
if total_bases == 0:
return 0.0
g_count = seq_upper.count("G")
c_count = seq_upper.count("C")
return ((g_count + c_count) / total_bases) * 100.0
gene = "ATGCGATCGATCGATCGATCGATCGATCGATAGCTAGCTAGCTA"
gc = calculate_gc_content(gene)
print(f"GC-Content: {gc:.2f}%")
Expected Output:
GC-Content: 47.73%
Scanning server audit logs for warning and error occurrence thresholds:
def audit_log_severities(log_content: str) -> dict[str, int]:
return {
"CRITICAL": log_content.count("CRITICAL:"),
"ERROR": log_content.count("ERROR:"),
"WARNING": log_content.count("WARNING:"),
"INFO": log_content.count("INFO:")
}
server_logs = """
2026-10-13 INFO: Service started
2026-10-13 WARNING: High memory usage
2026-10-13 ERROR: DB Connection timed out
2026-10-13 ERROR: Failed to retry DB connection
2026-10-13 CRITICAL: Out of memory, process terminated
"""
stats = audit_log_severities(server_logs)
print("Audit summary:", stats)
Expected Output:
Audit summary: {'CRITICAL': 1, 'ERROR': 2, 'WARNING': 1, 'INFO': 1}
Rapidly checking whether mathematical or code expressions have matching opening and closing delimiters:
def check_delimiter_parity(expr: str) -> bool:
# Quick count heuristic: opening count must equal closing count
parens_ok = expr.count("(") == expr.count(")")
brackets_ok = expr.count("[") == expr.count("]")
braces_ok = expr.count("{") == expr.count("}")
return parens_ok and brackets_ok and braces_ok
print("Valid expr: ", check_delimiter_parity("((a + b) * [c - d])"))
print("Invalid expr:", check_delimiter_parity("((a + b) * [c - d]"))
Expected Output:
Valid expr: True
Invalid expr: False
When overlapping occurrences must be counted, regular expression lookahead captures every occurrence:
import re
def count_overlapping(text: str, pattern: str) -> int:
# (?=(...)) lookahead assertion matches without consuming characters
escaped = re.escape(pattern)
return len(re.findall(rf"(?=({escaped}))", text))
sample = "aaaa"
print("str.count('aa'): ", sample.count("aa"))
print("count_overlapping('aa'): ", count_overlapping(sample, "aa"))
Expected Output:
str.count('aa'): 2
count_overlapping('aa'): 3
str.count() performs exact character matching. It will not find uppercase variations. If case-insensitive frequency is needed, normalize first: text.lower().count("python").
Relying on str.count() to count occurrences in repetitive motifs (like DNA codons or sliding window patterns) will undercount if occurrences share common characters. Use a sliding window or regex lookaheads when overlaps are significant.
Below is a production-grade utility class, SubstringCounter, demonstrating GC calculation, log counting, bracket balance validation, and automated unit assertions:
class SubstringCounter:
@staticmethod
def count_case_insensitive(text: str, sub: str) -> int:
"""Counts substring occurrences ignoring case."""
return text.lower().count(sub.lower())
@staticmethod
def gc_percentage(dna: str) -> float:
"""Computes GC content percentage."""
s = dna.upper()
if not s:
return 0.0
return ((s.count("G") + s.count("C")) / len(s)) * 100.0
@staticmethod
def is_balanced_parens(expr: str) -> bool:
"""Checks parentheses parity via count."""
return expr.count("(") == expr.count(")")
# ==========================================
# SELF-CHECKING UNIT ASSERTIONS
# ==========================================
counter = SubstringCounter()
# 1. Non-overlapping counting
assert "aaaa".count("aa") == 2
assert "banana".count("ana") == 1
# 2. Slice bounds [start:end]
assert "abc_abc_abc".count("abc", 4) == 2
assert "abc_abc_abc".count("abc", 4, 7) == 1
# 3. Empty string occurrence rule (N + 1)
assert "hello".count("") == 6
assert "".count("") == 1
# 4. Case-insensitive counting
assert counter.count_case_insensitive("Python python PYTHON", "python") == 3
# 5. GC content calculation
assert round(counter.gc_percentage("GCGCAT"), 2) == 66.67
assert counter.gc_percentage("") == 0.0
# 6. Parentheses parity
assert counter.is_balanced_parens("(a + (b * c))") is True
assert counter.is_balanced_parens("(a + b") is False
print("Case-insensitive count: ", counter.count_case_insensitive("Dev dev DEV", "dev"))
print("GC percentage: ", f"{counter.gc_percentage('ATGC'):.1f}%")
print("All str.count() unit assertions passed successfully!")
Open in Google Colab View on GitHub
Run all substring occurrence counting examples interactively, contrast non-overlapping count() with regex lookahead, calculate genomic GC content, and audit log files in Google Colab.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.