Use Pandas string case methods to transform text or build consistent comparison keys. Choose the operation that suits the task, preserve original spelling where it matters, and check missing or unknown labels before grouping data.
Show Table of Contents ↓Case methods change string values, not a web page’s font styling. upper() returns transformed values; assign the result if it should become a column.
import pandas as pd
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
print(df["NAME"].str.upper())
assert df["NAME"].str.upper().tolist() == ["RAVI", "RAJU", "ALEX"]Expected output
0 RAVI
1 RAJU
2 ALEX
Name: NAME, dtype: strlower() converts cased letters to lowercase. It preserves punctuation, digits and whitespace.
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
print(df["NAME"].str.lower())Expected output
0 ravi
1 raju
2 alex
Name: NAME, dtype: strtitle() uppercases word beginnings and lowercases the remaining cased letters. It is a mechanical transformation, not a name or brand formatting rule.
df = pd.DataFrame({"NAME": ["ravi ron", "raju geek", "alex john"]})
print(df["NAME"].str.title())Expected output
0 Ravi Ron
1 Raju Geek
2 Alex John
Name: NAME, dtype: strcapitalize() changes the first character to title case and lowercases the remaining letters. It does not capitalize every word.
df = pd.DataFrame({"NAME": ["ravi ron", "raju geek", "alex john"]})
print(df["NAME"].str.capitalize())
assert df["NAME"].str.capitalize().iloc[0] == "Ravi ron"Expected output
0 Ravi ron
1 Raju geek
2 Alex john
Name: NAME, dtype: strswapcase() exchanges the cases of cased characters. It is useful for demonstrations, but usually is not a business-data normalization rule. Unicode transformations need not be reversible.
df = pd.DataFrame({"NAME": ["Ravi Ron", "Raju Geek", "Alex John"]})
print(df["NAME"].str.swapcase())Expected output
0 rAVI rON
1 rAJU gEEK
2 aLEX jOHN
Name: NAME, dtype: strAcronyms and mixed-case brands can lose their intended form with title() or capitalize(). Preserve source spelling when names or branding matter.
labels = pd.Series(["NASA report", "iPhone case", "BLUE pen"], dtype="string")
comparison = pd.DataFrame({"source": labels})
for method in ["upper", "lower", "title", "capitalize", "swapcase"]:
comparison[method] = getattr(labels.str, method)()
print(comparison)Expected output
source upper lower title capitalize swapcase
0 NASA report NASA REPORT nasa report Nasa Report Nasa report nasa REPORT
1 iPhone case IPHONE CASE iphone case Iphone Case Iphone case IpHONE CASE
2 BLUE pen BLUE PEN blue pen Blue Pen Blue pen blue PENNullable string storage preserves missing values through case conversion. Converting a number to text is a policy choice, not a default cleaning step. Detect unexpected input types before casting if the field must contain text. See astype().
raw = pd.Series(["Alex", None, 120], dtype="object")
unexpected = raw.notna() & ~raw.map(lambda value: isinstance(value, str))
print("Unexpected values:", raw.loc[unexpected].tolist())
text = raw.astype("string")
print(text.str.lower())
assert text.str.lower().isna().sum() == 1Expected output
Unexpected values: [120]
0 alex
1 <NA>
2 120
dtype: stringCase conversion does not remove spaces. strip() removes surrounding whitespace; a regex replacement can collapse repeated interior whitespace. Keep the raw value for comparison; see str.replace().
labels = pd.Series([" BLUE pen ", "Notebook A5", None], dtype="string")
clean = labels.str.strip().str.replace(r"\s+", " ", regex=True).str.lower()
print(clean)
assert clean.iloc[0] == "blue pen"Expected output
0 blue pen
1 notebook a5
2 <NA>
dtype: stringcasefold() removes case distinctions more aggressively than lower(), including the German sharp s. It is useful for comparison keys when the domain treats these spellings as equivalent. It is not locale-specific display formatting.
labels = pd.Series(["Straße", "STRASSE"], dtype="string")
print("Lower:", labels.str.lower().tolist())
print("Casefold:", labels.str.casefold().tolist())
assert labels.str.casefold().nunique() == 1Expected output
Lower: ['straße', 'strasse']
Casefold: ['strasse', 'strasse']Visually similar text may use different Unicode sequences. NFC normalization followed by casefold can provide a comparison key. Do not treat this as proof that records describe the same entity.
labels = pd.Series(["CAFÉ", "cafe\u0301"], dtype="string")
keys = labels.str.normalize("NFC").str.casefold()
print(keys.tolist())
assert keys.nunique() == 1Expected output
['café', 'café']This synthetic extract contains case variations, spaces, a blank and an unknown label. Create a normalized key, then map a controlled vocabulary to canonical labels. Do not title-case every value and assume it belongs to an approved category.
sales = pd.DataFrame({"order_id": [101, 102, 103, 104, 105, 106], "region": [" NORTH ", "north", "South", "", None, "Northeast"], "amount": [10, 20, 30, 4, 5, 6]})
sales["region_key"] = sales["region"].astype("string").str.strip().str.casefold().replace("", pd.NA)
approved = {"north": "North", "south": "South"}
sales["canonical_region"] = sales["region_key"].map(approved).astype("string")
unknown = sales["region_key"].notna() & sales["canonical_region"].isna()
print(sales)
print("Unknown labels:", sales.loc[unknown, "region"].tolist())
assert sales.loc[unknown, "region"].tolist() == ["Northeast"]Expected output
order_id region amount region_key canonical_region
0 101 NORTH 10 north North
1 102 north 20 north North
2 103 South 30 south South
3 104 4 <NA> <NA>
4 105 NaN 5 <NA> <NA>
5 106 Northeast 6 northeast <NA>
Unknown labels: ['Northeast']An unknown label differs from a missing label. Keep both visible during aggregation instead of silently losing their rows. Case normalization should not alter amounts or remove legitimate orders. See count() and sum().
print("Missing region keys:", sales["region_key"].isna().sum())
print("Unknown labels:", unknown.sum())
print("Totals including review group:")
totals = sales.groupby("canonical_region", dropna=False)["amount"].sum()
print(totals)
assert totals.sum() == sales["amount"].sum()
# Optional Colab export:
# sales.to_csv("sales_region_review.csv", index=False)Expected output
Missing region keys: 2
Unknown labels: 1
Totals including review group:
canonical_region
North 30
South 30
<NA> 15
Name: amount, dtype: int64Several raw labels can map to one key. This may be intended, but deduplicating whole records on that key would remove legitimate sales. Inspect variants before deciding; see drop_duplicates().
variants = sales.dropna(subset=["region_key"]).groupby("region_key")["region"].nunique()
print("Raw variants per normalized key:")
print(variants)
assert variants.loc["north"] == 2Expected output
Raw variants per normalized key:
region_key
north 2
northeast 1
south 1
Name: region, dtype: int64For a search, str.contains() with case=False may be enough. There is no need to rewrite stored product names solely to perform a case-insensitive search.
products = pd.Series(["Blue Pen", "PEN refill", "Notebook", None], dtype="string")
mask = products.str.contains("pen", case=False, regex=False, na=False)
print(products.loc[mask].tolist())
assert products.iloc[0] == "Blue Pen"Expected output
['Blue Pen', 'PEN refill']Do these methods change font color? No; they change text values. Does lower() remove spaces? No; strip or replace whitespace separately. Why did NASA become Nasa? Title casing is mechanical and does not preserve acronyms. Can casefold affect length? Yes; some characters expand, so check str.len() after conversion when necessary. Should identifiers be lowercased? Only when their specification is case-insensitive; URLs and product codes may be case-sensitive.
Create statuses " PAID ", "paid", "Pending", None and "other". Normalize a comparison key, map paid and pending to canonical labels, and report the unknown non-missing label. Keep the original status values.
Mapping a defined vocabulary keeps unknown input visible instead of assuming every title-cased value is valid.
orders = pd.DataFrame({"status": [" PAID ", "paid", "Pending", None, "other"]})
orders["key"] = orders["status"].astype("string").str.strip().str.casefold()
orders["canonical"] = orders["key"].map({"paid": "Paid", "pending": "Pending"}).astype("string")
unknown = orders["key"].notna() & orders["canonical"].isna()
print(orders)
print("Unknown:", orders.loc[unknown, "status"].tolist())
assert orders.loc[unknown, "status"].tolist() == ["other"]Expected output
status key canonical
0 PAID paid Paid
1 paid paid Paid
2 Pending pending Pending
3 NaN <NA> <NA>
4 other other <NA>
Unknown: ['other']Open in Google Colab View on GitHub
Run the examples in order, change source labels and compare normalized keys, canonical values and review rows. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with data cleaning, splitting text and string slicing.
Reference: Pandas string casefold 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.