Text and String Methods (str accessor and regex)
Why not just use Python string methods?
Python’s built-in string methods (.lower().lower(), .split().split(), .replace().replace()) work great on
a single string. But a DataFrame column can hold thousands of strings — and, almost
always, a few missing values mixed in. Calling .lower().lower() directly on every value with
.map().map() works fine until it hits a NaNNaN, and then it crashes.
Pandas solves this with the strstr accessor: series.str.lower()series.str.lower() runs a
vectorized, element-wise version of the string method across the whole column, and
automatically skips (propagates) missing values instead of erroring out.
Example dataset
import pandas as pd
emails = pd.Series(
["Dave@Google.com", "steve@gmail.com ", None, " rob@gmail.com"],
index=["Dave", "Steve", "Wes", "Rob"],
)
print(emails)
print(emails.isna())import pandas as pd
emails = pd.Series(
["Dave@Google.com", "steve@gmail.com ", None, " rob@gmail.com"],
index=["Dave", "Steve", "Wes", "Rob"],
)
print(emails)
print(emails.isna())Everyday cleanup: lower, strip, contains, replace
print(emails.str.lower())
print(emails.str.strip().str.lower())print(emails.str.lower())
print(emails.str.strip().str.lower())str.containsstr.contains checks each value for a substring (or pattern) and returns a
boolean Series — pass na=Falsena=False so missing values count as “no match” instead of
showing up as NaNNaN in a mask:
has_gmail = emails.str.contains("gmail", case=False, na=False)
print(has_gmail)
print(emails[has_gmail])has_gmail = emails.str.contains("gmail", case=False, na=False)
print(has_gmail)
print(emails[has_gmail])str.replacestr.replace swaps out a substring everywhere it appears — pass regex=Falseregex=False when
your search text isn’t a pattern, so characters like .. aren’t treated as “any
character”:
print(emails.str.replace("gmail", "GMAIL", regex=False))print(emails.str.replace("gmail", "GMAIL", regex=False))Splitting and pulling pieces out
str.splitstr.split breaks each string into a list at a delimiter; indexing into the result
with .str[i].str[i] or .str.get(i).str.get(i) pulls out one piece of each list:
parts = emails.str.strip().str.split("@")
print(parts)
print(parts.str.get(0)) # username, before the @
print(parts.str.get(1)) # domain, after the @parts = emails.str.strip().str.split("@")
print(parts)
print(parts.str.get(0)) # username, before the @
print(parts.str.get(1)) # domain, after the @Slicing works too, the same way you’d slice a plain Python string:
print(emails.str.strip().str[:4])print(emails.str.strip().str[:4])Regex with .str.extract.str.extract and .str.findall.str.findall
For real pattern matching — not just a fixed substring — pass a regular expression.
str.extractstr.extract pulls out groups (the parts in parentheses) into their own columns;
str.findallstr.findall returns every match as a list:
import re
pattern = r"([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+)\.([A-Za-z]{2,4})"
clean_emails = emails.str.strip()
print(clean_emails.str.extract(pattern))import re
pattern = r"([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+)\.([A-Za-z]{2,4})"
clean_emails = emails.str.strip()
print(clean_emails.str.extract(pattern))The result is a small DataFrame with one column per capture group — column 00 is the
username, 11 is the domain, 22 is the suffix. Rows that don’t match (or were NaNNaN
to begin with) come back as NaNNaN in every column, not an error.
print(clean_emails.str.findall(pattern, flags=re.IGNORECASE))print(clean_emails.str.findall(pattern, flags=re.IGNORECASE))Common pitfalls
- Forgetting
regex=Falseregex=Falseonstr.replacestr.replacewhen your replacement text contains regex special characters like..or((— it will silently match more than you intended. - Boolean masks from
str.containsstr.containscan containNaNNaN(for missing input) unless you passna=Falsena=False— filtering with a mask that still hasNaNNaNin it raises an error. str.extractstr.extractneeds parentheses(...)(...)around the parts you want back; a pattern with no groups extracts nothing useful.
Visualize it
flowchart LR A["Series with NaN mixed in"] --> B[".str accessor"] B --> C["Real values -> string/regex logic"] B --> D["NaN -> stays NaN"] C --> E["Cleaned Series"] D --> E
🧪 Try It Yourself
Exercise 1 – Lowercase an Entire Column
Exercise 2 – Check Membership While Ignoring NaN
Exercise 3 – Extract a Regex Group
Next
With text under control, move on to Reading and Writing Data (CSV, Excel, JSON) to load real datasets that will need exactly this kind of cleanup.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
