Skip to content

pandas str.title

The friendliest-looking method in the string accessor, and the one most likely to put a typo into your chart.

What it does

Have you ever fixed a column's capitalization and then found the mistake in the published version? .str.title() capitalizes the first letter of every word and lowercases the rest, one call, no arguments. It looks like the polite way to tidy up a shouting column of text.

It is not a display method. It is a folding method that happens to produce readable output, and the difference matters. Its one real job is the job str.lower does: making values that ought to be equal compare as equal, in value_counts, in groupby, in a merge. The only thing it adds over lowercasing is that the folded key is still legible on an axis label. Everything that goes wrong with this method goes wrong because somebody wanted that second property and got the first one's accuracy.

Three words are enough to see the problem, and all three are common in real data:

s = pd.Series(["MCDONALD'S", 'KFC', 'USA'])

pd.DataFrame({'original': s, 'title': s.str.title(),
              'capitalize': s.str.capitalize()})
     original       title  capitalize
0  MCDONALD'S  Mcdonald'S  Mcdonald's
1         KFC         Kfc         Kfc
2         USA         Usa         Usa

Python treats the apostrophe as a word break, so Mcdonald'S. It has no idea that KFC and USA are acronyms, or that Mc is a prefix. .str.capitalize() — the sibling that capitalizes only the first letter of the whole value — gets the apostrophe right and is otherwise no better.

Official documentation: Series.str.title and Series.str.capitalize

A worked example, on real data

New York City publishes every 311 service request. Here is January 2025, where the complaint categories have been typed in by hand for years:

import pandas as pd

url = ('https://data.cityofnewyork.us/resource/erm2-nwe9.csv'
       '?$select=complaint_type,agency,agency_name'
       "&$where=date_trunc_ym(created_date)='2025-01'"
       '&$limit=300000')

df = pd.read_csv(url)

df['complaint_type'].value_counts().head(8)
complaint_type
HEAT/HOT WATER          60567
Noise - Residential     54301
Illegal Parking         37570
Blocked Driveway        12797
UNSANITARY CONDITION     8864
PLUMBING                 6673
Water System             6140
Snow or Ice              5144
Name: count, dtype: int64

Some categories shout and some do not, which is the classic reason to fold. And here .str.title() earns its keep, because two categories are split in two:

df['complaint_type'].value_counts().reindex(['PLUMBING', 'Plumbing',
                                             'ELEVATOR', 'Elevator'])
complaint_type
PLUMBING    6673
Plumbing     231
ELEVATOR     107
Elevator    1432
Name: count, dtype: int64

Neither 6,673 nor 231 is the number of plumbing complaints. Fold the column and they merge, into 6,904 and 1,539 — and unlike .str.lower(), the result is still something you can put on a chart.

Now look at what the same call did to the rest of the column:

(df['complaint_type']
 .drop_duplicates()
 .to_frame()
 .assign(title=lambda df_: df_['complaint_type'].str.title())
 .loc[lambda df_: df_['complaint_type'].isin(
     ['HEAT/HOT WATER', 'PLUMBING', 'Smoking or Vaping',
      'Noise - House of Worship'])]
 .reset_index(drop=True))
             complaint_type                     title
0            HEAT/HOT WATER            Heat/Hot Water
1         Smoking or Vaping         Smoking Or Vaping
2  Noise - House of Worship  Noise - House Of Worship
3                  PLUMBING                  Plumbing

Smoking Or Vaping. House Of Worship. English title case lowercases short function words; Python's does not, because Python is not doing title case at all — it is uppercasing the letter after every non-letter. The one call that fixed PLUMBING broke or and of in the same pass.

Three mistakes people make

Titling a column of acronyms. The agency column is nothing but acronyms, and .str.title() destroys every one of them:

(df['agency'].value_counts().head(4)
 .to_frame().reset_index()
 .assign(title=lambda df_: df_['agency'].str.title()))
  agency   count title
0   NYPD  125494  Nypd
1    HPD   99366   Hpd
2   DSNY   23849  Dsny
3    DOT   14681   Dot

Nobody has ever called them Nypd. Where the values are codes, fold with .str.lower() and keep the original for display.

Titling names that already have correct capitalization. agency_name is written properly in the source, and .str.title() makes it worse:

names = ['Department of Buildings',
         'Department of Health and Mental Hygiene',
         'Taxi and Limousine Commission']

(df.loc[df['agency_name'].isin(names), 'agency_name']
 .drop_duplicates().sort_values().to_frame()
 .assign(title=lambda df_: df_['agency_name'].str.title())
 .reset_index(drop=True))
                               agency_name                                    title
0                  Department of Buildings                  Department Of Buildings
1  Department of Health and Mental Hygiene  Department Of Health And Mental Hygiene
2            Taxi and Limousine Commission            Taxi And Limousine Commission

If the column is already consistent, there is nothing to fold, and the only thing this call can do is introduce errors.

Trusting it in published output. This is not a hypothetical. All three Bamboo Weekly solutions below print .str.title() results, and all three carry the bug into the answer: Weapons And Equipment, District Of Columbia, Harvard School Of Public Health. I did not notice at the time either.

Where it shows up in Bamboo Weekly

Four Bamboo Weekly solutions use .str.title(), and none uses .str.capitalize() — which tells you something.

Bamboo Weekly #63: Ukraine aid is the honest case, free to read. A hand-typed Type of Aid Specific column held 34 distinct values; .str.title() brought it to 28 and adding .str.strip() brought it to 26. That is the method working. The published output is headed Weapons And Equipment, which is the method failing, in the same chain.

Bamboo Weekly #33: Fracking is where it is unambiguously right, and also free. A StateName column typed by hand gave 75 distinct values including Texas, tx and Ok. State names are ordinary words with no function words in them, so .str.title() folds them safely — and the leftovers, Texasa and Pennsylvanya, then need replace and a dictionary, because no string method will ever guess those.

Bamboo Weekly #105: Federal employees runs .str.title() on a location column of all-caps state names, and its results table is topped by District Of Columbia with 162,144 employees.

Practice it

Work through a .str.title() exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/str-title/

Go deeper

My rule: use .str.title() only when you need a folded key that will be read by a human, and only on values that are ordinary English words — no acronyms, no possessives, no of and no and. Everywhere else, str.lower folds just as well and cannot embarrass you, and a small dictionary through replace gives you display labels that are actually correct.

Before blaming case for anything, check the two problems that masquerade as it: str.strip for invisible whitespace, and str.replace for the punctuation that no amount of folding will fix.

More Pandas videos on Python and Pandas with Reuven Lerner.

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 4 Bamboo Weekly exercises that use str.title on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.