String operations with .str
The .str accessor, regex extraction, contains and replace, splitting columns apart, and cleaning text columns without a Python loop.
The .str accessor
s = df["name"].astype("string") # nullable string dtype
s.str.strip().str.title()
s.str.len()
s.str.upper()
s.str.contains("ada", case=False, na=False)
s.str.startswith("A").fillna(False)
s.str.zfill(6)
s.str.pad(10, side="right", fillchar=".")- Every method works element-wise and returns a Series, so operations chain without a loop.
.strrequires text; on a mixed column cast first withastype("string")or the accessor raises.- Start and end matching with
str.startswithis faster than the equivalent regex for simple prefixes. - Boolean results inherit
NaNfrom missing input, which breaks filters - passna=Falseor.fillna(False).
Extraction and replacement with regex
df["clean"] = df["text"].str.replace(r"\s+", " ", regex=True).str.strip()
df["digits"] = df["code"].str.extract(r"(\d+)", expand=False)
parsed = df["email"].str.extract(r"(?P<user>[^@]+)@(?P<domain>.+)$")
df["user"] = parsed["user"]
df["domain"] = parsed["domain"]
df["tag_list"] = df["tags"].str.findall(r"#(\w+)")
df["has_vip"] = df["tags"].str.contains(r"#vip\b", regex=True, na=False)
df["masked"] = df["phone"].str.replace(r"\d(?=\d{3})", "*", regex=True)| Method | Returns |
|---|---|
contains | Boolean Series - one flag per row |
extract | DataFrame of named groups, or a Series with expand=False |
extractall | One row per match, keeping the original index |
findall | A list of all matches per row |
replace | The substituted strings |
split | A list, or columns with expand=True |
⚠️
A regex that fails to match yields
NaN, not an error. Count matches after every extraction - df["digits"].isna().sum() - because a silently unmatched pattern looks exactly like a clean column of missing values.Worked example: cleaning a name column
names = df["full_name"].astype("string")
parts = names.str.strip().str.replace(r"\s+", " ", regex=True).str.split(" ", n=1, expand=True)
df["first"] = parts[0].str.title()
df["last"] = parts[1].fillna("").str.title()
titles = r"^(mr|mrs|ms|dr)\.?\s+"
df["first"] = (
df["first"]
.str.replace(titles, "", regex=True)
.str.strip()
.str.replace(r"[^\p{L}\-']", "", regex=True) # keep letters, hyphen, apostrophe
)
df["initials"] = (
df["first"].str[0].fillna("") + df["last"].str[0].fillna("")
).str.upper()
print(df[["first", "last", "initials"]].head())
print(df["first"].isna().sum(), "rows without a first name")Build the pipeline in small steps and print the intermediate result after each one. Text cleaning fails by producing a plausible-looking column that is subtly wrong, so an eyeball check on real rows matters more here than in numeric work.
FAQ
Why do my boolean string filters drop rows?
A
contains result is NaN wherever the input was missing, and NaN is falsy in a mask. Pass na=False.object or string dtype?
The
string dtype is explicit about being text and stores missing values as pd.NA. object is a mixed bag that may hold anything, so prefer string for real text columns.Related
Debugging pandas: SettingWithCopy, dtype and index surprises Categoricals, dtypes and memory optimisation
Last refreshed 2026-09-18.