Use str.count(pat, flags=0) to count regex pattern matches within each string. Escape literal search terms, choose case rules and preserve missing input. This differs from counting non-missing values or counting rows that contain a match.
The pattern is a regular expression. Matching is case-sensitive by default: lowercase a does not match the uppercase A in Alex. The result has one count per input value.
import pandas as pd
import re
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
print(df["NAME"].str.count(pat="a"))
assert df["NAME"].str.count("a").tolist() == [1, 1, 0]Expected output
0 1
1 1
2 0
Name: NAME, dtype: int64Assign the returned Series to keep the count beside its source. This does not alter the source text.
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
df["number"] = df["NAME"].str.count(pat="a")
print(df)
assert df["number"].tolist() == [1, 1, 0]Expected output
NAME number
0 Ravi 1
1 Raju 1
2 Alex 0Use flags=re.IGNORECASE, or re.I, to match both cases. The parameter name is flags, not flag; count() has no case=False parameter.
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
df["number"] = df["NAME"].str.count("r", flags=re.IGNORECASE)
print(df)
assert df["number"].tolist() == [1, 1, 0]Expected output
NAME number
0 Ravi 1
1 Raju 1
2 Alex 0A dollar sign means end-of-string in regex. Use a raw escaped pattern to count literal dollar signs. For a literal term supplied at runtime, re.escape() handles regex metacharacters.
df = pd.DataFrame({"NAME": ["Ravi$a", "Raj$u$b", "Alex"]})
df["number"] = df["NAME"].str.count(pat=r"\$")
print(df)
assert df["number"].tolist() == [1, 2, 0]
text = pd.Series(["C++ and C++", "A.B", "AxB"], dtype="string")
print(text.str.count(re.escape("C++")))
print(text.str.count(re.escape("A.B")))Expected output
NAME number
0 Ravi$a 1
1 Raj$u$b 2
2 Alex 0
0 2
1 0
2 0
dtype: Int64
0 0
1 1
2 0
dtype: Int64sum() adds occurrences, not the number of rows with a match. In this case each matching row has one lowercase r. See sum().
names = pd.Series(["John", "Mac", "Arnold", "Krish", "Roni"], name="Names")
counts = names.str.count("r")
print(counts)
print("Total occurrences:", counts.sum())
assert counts.tolist() == [0, 0, 1, 1, 0]
assert counts.sum() == 2Expected output
0 0
1 0
2 1
3 1
4 0
Name: Names, dtype: int64
Total occurrences: 2DataFrame.count() counts non-missing values. str.count() counts pattern matches in each string. str.contains().sum() counts matching rows. These answer different questions; see DataFrame count() and matching-row totals.
text = pd.Series(["pen pen", "pen", "pencil", None], dtype="string")
counts = text.str.count("pen")
print("Occurrences:", counts.sum())
print("Matching rows:", text.str.contains("pen", regex=False, na=False).sum())
print("Present values:", text.count())
assert counts.sum() == 4
assert text.str.contains("pen", regex=False, na=False).sum() == 3Expected output
Occurrences: 4
Matching rows: 3
Present values: 3Missing text produces a missing count, while empty strings produce zero for a non-empty literal pattern. Nullable string conversion preserves missing input. Do not treat unknown text as a confirmed zero without a deliberate policy.
text = pd.Series(["pen", "", None, 120], dtype="object").astype("string")
counts = text.str.count("pen")
print(counts)
print("Missing input:", text.isna().sum())
assert counts.iloc[0] == 1 and counts.iloc[1] == 0
assert pd.isna(counts.iloc[2])
print("Display with missing counts filled:")
print(counts.fillna(0))Expected output
0 1
1 0
2 <NA>
3 0
dtype: Int64
Missing input: 1
Display with missing counts filled:
0 1
1 0
2 0
3 0
dtype: Int64Word boundaries exclude pencil and open from a search for pen. Regex boundaries follow regex word-character rules; they are not a language-aware tokenizer. Keep the pattern appropriate for the text.
text = pd.Series(["pen pen", "PEN refill", "pencil", "open", "pen-holder"], dtype="string")
print(text.str.count(r"\bpen\b", flags=re.IGNORECASE))
assert text.str.count(r"\bpen\b", flags=re.IGNORECASE).tolist() == [2, 1, 0, 0, 1]Expected output
0 2
1 1
2 0
3 0
4 1
dtype: Int64Ordinary matches do not overlap: aa appears twice non-overlapping in aaaa. A lookahead can count overlapping starting positions, but that is a different definition and should be explicit.
text = pd.Series(["aaaa", "aaa", "aa"], dtype="string")
print("Non-overlapping:")
print(text.str.count("aa"))
print("Overlapping starts:")
print(text.str.count(r"(?=aa)"))
assert text.str.count("aa").tolist() == [2, 1, 1]
assert text.str.count(r"(?=aa)").tolist() == [3, 2, 1]Expected output
Non-overlapping:
0 2
1 1
2 1
dtype: Int64
Overlapping starts:
0 3
1 2
2 1
dtype: Int64A character class matches one character per occurrence; a + quantifier matches a whole run. Do not use an empty pattern to count characters: it matches positions between characters, including both ends. Use str.len() for length.
text = pd.Series(["AB12-CD345", "plain", "7"], dtype="string")
print("Digit characters:")
print(text.str.count(r"[0-9]"))
print("Digit runs:")
print(text.str.count(r"[0-9]+"))
assert text.str.count(r"[0-9]").tolist() == [5, 0, 1]
assert text.str.count(r"[0-9]+").tolist() == [2, 0, 1]Expected output
Digit characters:
0 5
1 0
2 1
dtype: Int64
Digit runs:
0 2
1 0
2 1
dtype: Int64This synthetic schema requires exactly one literal pipe separating code and description. Count separators before splitting so an extra delimiter is not hidden in the remainder. Missing input remains a separate review reason.
raw = pd.DataFrame({"product": ["0012|Notebook", "0045 | Pen", "0070", "0081|Pencil|extra", None, "|Mug"]})
text = raw["product"].astype("string")
separator_counts = text.str.count(re.escape("|"))
structure_ok = separator_counts.eq(1).fillna(False)
accepted = raw.loc[structure_ok].copy()
parts = text.loc[structure_ok].str.split("|", n=1, expand=True, regex=False).reindex(columns=[0, 1])
accepted["code"] = parts[0].str.strip()
accepted["description"] = parts[1].str.strip()
fields_ok = (accepted["code"].str.fullmatch(r"[0-9]{4}", na=False)
& accepted["description"].notna() & accepted["description"].ne("")).fillna(False)
clean = accepted.loc[fields_ok].copy()
print(clean)
print("Missing inputs:", int(text.isna().sum()))
print("Present inputs with wrong separator count:", int((text.notna() & ~structure_ok).sum()))
print("Well-structured rows with invalid fields:", int((~fields_ok).sum()))
assert clean["code"].tolist() == ["0012", "0045"]Expected output
product code description
0 0012|Notebook 0012 Notebook
1 0045 | Pen 0045 Pen
Missing inputs: 1
Present inputs with wrong separator count: 2
Well-structured rows with invalid fields: 1Structure checks are only the first stage; empty or malformed pieces still need validation. Preserve the original row index to identify rejected records. See splitting text and the cleaning workflow.
review = raw.loc[~raw.index.isin(clean.index)].copy()
print("Review rows:")
print(review)
print("Accepted rows:", len(clean), "Review rows:", len(review))
assert len(clean) == 2 and len(review) == 4
print(clean.to_csv(index=False).rstrip())
# Optional Colab export:
# clean.to_csv("validated_product_fields.csv", index=False)Expected output
Review rows:
product
2 0070
3 0081|Pencil|extra
4 NaN
5 |Mug
Accepted rows: 2 Review rows: 4
product,code,description
0012|Notebook,0012,Notebook
0045 | Pen,0045,PenThe string accessor also works on an Index. Inspect label structure separately from values inside a DataFrame.
labels = pd.Index(["part_A_B", "part_C", "plain"], name="field")
print(labels.str.count("_"))
assert labels.str.count("_").tolist() == [2, 1, 0]Expected output
Index([2, 1, 0], dtype='int64', name='field')Does count() join text? No; use str.cat() for concatenation. Can I pass regex=False? str.count() has no regex=False option; escape literal text with re.escape(). Why did a dot match every character? A dot is a regex wildcard. Why did totals exclude missing text? sum() skips missing counts by default; report missing inputs separately. If every input is missing, sum(min_count=1) retains an unknown total. Does counting separators validate CSV? No; quoted delimiters require a CSV parser.
Create values A+B+C, plain, ++ and a missing value. Count literal plus signs, report total known occurrences and the number of missing inputs. Expect counts 2, 0, 2 and missing; total four.
Escape the plus sign because it is a regex quantifier. Keep the missing count visible rather than treating it as known zero.
practice = pd.Series(["A+B+C", "plain", "++", None], dtype="string")
answer = practice.str.count(re.escape("+"))
print(answer)
print("Known occurrences:", answer.sum(min_count=1))
print("Missing inputs:", practice.isna().sum())
assert answer.iloc[:3].tolist() == [2, 0, 2]
assert answer.sum() == 4 and pd.isna(answer.iloc[3])Expected output
0 2
1 0
2 2
3 <NA>
dtype: Int64
Known occurrences: 4
Missing inputs: 1Open in Google Colab View on GitHub
Run the examples in order, change patterns, flags and source text, then compare occurrence counts and rejected rows. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with the Data Cleaning and String Operations hub, matching text, string length and replacing text.
Reference: Pandas Series.str.count documentation.
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.