Python Central HubSign in

Data Analytics with Python / Phase 3: Data Manipulation with Pandas

Text and String Methods (str accessor and regex)

Clean and extract text with pandas' str accessor - lower/upper/strip/contains/replace/split/extract - and use regex safely on columns that contain missing values.

Why not just use Python string methods?

Python's built-in string methods (.lower(), .split(), .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() directly on every value with .map() works fine until it hits a NaN, and then it crashes.

Pandas solves this with the str accessor: 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

Python
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

Python
print(emails.str.lower())
print(emails.str.strip().str.lower())

str.contains checks each value for a substring (or pattern) and returns a boolean Series — pass na=False so missing values count as "no match" instead of showing up as NaN in a mask:

Python
has_gmail = emails.str.contains("gmail", case=False, na=False)
print(has_gmail)
print(emails[has_gmail])

str.replace swaps out a substring everywhere it appears — pass regex=False when your search text isn't a pattern, so characters like . aren't treated as "any character":

Python
print(emails.str.replace("gmail", "GMAIL", regex=False))

Splitting and pulling pieces out

str.split breaks each string into a list at a delimiter; indexing into the result with .str[i] or .str.get(i) pulls out one piece of each list:

Python
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:

Python
print(emails.str.strip().str[:4])

Regex with .str.extract and .str.findall

For real pattern matching — not just a fixed substring — pass a regular expression. str.extract pulls out groups (the parts in parentheses) into their own columns; str.findall returns every match as a list:

Python
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 0 is the username, 1 is the domain, 2 is the suffix. Rows that don't match (or were NaN to begin with) come back as NaN in every column, not an error.

Python
print(clean_emails.str.findall(pattern, flags=re.IGNORECASE))

Common pitfalls

  • Forgetting regex=False on str.replace when your replacement text contains regex special characters like . or ( — it will silently match more than you intended.
  • Boolean masks from str.contains can contain NaN (for missing input) unless you pass na=False — filtering with a mask that still has NaN in it raises an error.
  • str.extract needs parentheses (...) around the parts you want back; a pattern with no groups extracts nothing useful.

Visualize it

diagram The str accessor skips missing values mermaid
Every str method runs element-wise down a column, passing real values through the string/regex logic and letting NaN pass straight through untouched.

🧪 Try It Yourself

Exercise 1 – Lowercase an Entire Column

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Check Membership While Ignoring NaN

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Extract a Regex Group

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Check your understanding

quizCheck yourself
  1. Why use emails.str.lower() instead of emails.map(lambda x: x.lower())?

    Show answer

    A — The str accessor propagates missing values instead of crashing on them — Calling .lower() on a None or NaN value crashes inside map. The str accessor skips missing values.

  2. Why pass na=False to str.contains("gmail", case=False, na=False)?

    Show answer

    A — So missing values count as no match instead of NaN in the mask — A mask containing NaN raises an error when used for filtering. na=False makes missing values False.

  3. After parts = emails.str.strip().str.split("@"), what does parts.str.get(1) return for steve@gmail.com?

    Show answer

    A — gmail.com — split breaks the string at the @, and get(1) takes the second piece, the domain. get(0) would be the username.

  4. What does str.extract do with a regex pattern?

    Show answer

    A — Returns the captured groups as columns of a DataFrame — extract pulls out the parenthesized groups, one column per group, and unmatched rows give NaN. findall returns every match as a list.

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.

Was this page helpful?

Let us know how we did