Not how long your values are. Which of them are the wrong length.
What it does
How do you find the broken rows in a column you did not create? You cannot read ten thousand values, and you cannot write a rule against a problem you have not seen yet. But most broken values announce themselves the same way: they are the wrong size. The postal code truncated to four digits, the ID that lost its leading zeros on the way through Excel, the short-answer field somebody pasted three paragraphs into.
.str.len() returns a new series holding the size of every element, and on its own that number is rarely interesting — I have never needed to know that NVIDIA CORP is eleven characters long. What I need is the row where the number is impossible. This is not a measurement tool, it is a validation tool, and the idiom is .str.len().value_counts().
Official documentation: Series.str.len
It takes no arguments
There is nothing to configure. The only thing worth knowing is what it counts, and that depends on what the column holds:
- a string: the number of characters
- a list — which is what str.split hands back by default — the number of elements
- a dict: the number of keys
- a missing value: another missing value
Same call, four meanings. The last one is the one that will cost you an afternoon, and I will come back to it.
A worked example, on real data
The SEC publishes the mapping from every registered company to its ticker symbol and exchange. It asks that you identify yourself in the User-Agent header, and returns 403 if you do not:
import pandas as pd
url = 'https://www.sec.gov/files/company_tickers_exchange.json'
ua = {'User-Agent': 'Your Name your@email.com'}
raw = pd.read_json(url, storage_options=ua, typ='series')
df = pd.DataFrame(raw['data'], columns=raw['fields'])
df.head()
cik name ticker exchange
0 1045810 NVIDIA CORP NVDA Nasdaq
1 320193 Apple Inc. AAPL Nasdaq
2 1652044 Alphabet Inc. GOOGL Nasdaq
3 789019 MICROSOFT CORP MSFT Nasdaq
4 1018724 AMAZON COM INC AMZN Nasdaq
10,403 rows the day I ran it; the SEC refreshes the file nightly, so your counts will be close rather than identical. Before I join this against anything, I want to know whether the ticker column is what I think it is:
df['ticker'].str.len().value_counts().sort_index()
ticker
1 21
2 247
3 2014
4 4989
5 2654
6 299
7 179
Name: count, dtype: int64
US ticker symbols run one to five characters. There are 478 rows here that are longer than that, and every one of them is a finding. Pull them out with .loc:
df.loc[pd.col('ticker').str.len() > 5, ['name', 'ticker']].head(5)
name ticker
6748 CONSUMERS ENERGY CO CMS-PB
6962 Brookfield Oaktree Holdings, LLC OAK-PA
7032 Seritage Growth Properties SRG-PA
7040 InPoint Commercial Real Estate Income, Inc. ICR-PA
7350 Kensington Capital Acquisition Corp. VI KCAC-UN
Not common stock. Those are preferred series and unit lines, with the class appended after a hyphen. Joined against a price feed keyed on plain symbols, 478 rows would have failed to match in silence — and one line found them without my knowing in advance that share classes existed.
The check also tells me what rule to write next. Length was a screen, not the answer: BRK-B is five characters and looks exactly like GOOGL. Counting pieces rather than characters catches all of them:
df['ticker'].str.split('-').str.len().value_counts()
ticker
1 9861
2 542
Name: count, dtype: int64
542, not 478. Same method, one .str.split() earlier in the chain, and now it is counting elements in a list instead of characters in a string.
Now the identifier. A CIK is the SEC's company number, and EDGAR's file paths want it zero-padded to ten digits:
df['cik'].astype(str).str.len().value_counts().sort_index()
cik
4 58
5 509
6 1453
7 8383
Name: count, dtype: int64
Four lengths in a fixed-width identifier column. That is the shape of the leading-zero bug: the field was numeric somewhere upstream, the zeros went away, and nothing complained. A well-formed identifier column shows exactly one length, which is what makes this such a fast test — you are looking for a one-row answer, and anything else is the finding.
(
df
.assign(cik=pd.col('cik').astype(str).str.zfill(10))
.head(3)
)
cik name ticker exchange
0 0001045810 NVIDIA CORP NVDA Nasdaq
1 0000320193 Apple Inc. AAPL Nasdaq
2 0001652044 Alphabet Inc. GOOGL Nasdaq
One length, 10,403 times. Now it joins.
Four mistakes people make
Letting one missing value turn the whole column into floats. exchange has 198 gaps, and .str.len() propagates each one rather than calling it zero:
df['exchange'].str.len().value_counts(dropna=False)
exchange
6.0 4365
4.0 3336
3.0 2504
NaN 198
Name: count, dtype: int64
Lengths of 6.0 and 4.0, because NaN is a float and one of them drags the whole series to float64. That is the right answer — an unknown string has an unknown length, not a length of zero — but it breaks the two things you were about to do next. astype(int) raises IntCastingNaNError, and a groupby on the lengths drops those rows without a word:
df.groupby(df['exchange'].str.len()).size().sum()
10205
198 companies short of 10,403, and no warning anywhere. Reach for .astype('Int64') — the nullable integer type, capital I — when you want whole numbers and want the gaps to survive.
Counting characters when you meant elements, or the reverse. A column of lists prints almost exactly like a column of strings, so the only sign you are on the wrong one is that the number is implausible:
df['name'].str.len().head(3)
0 11
1 10
2 13
Name: name, dtype: int64
df['name'].str.split().str.len().head(3)
0 2
1 2
2 2
Name: name, dtype: int64
Both are correct answers to different questions. Check the dtype of the series you are measuring: str means characters, object almost always means lists. On a series of dicts it counts keys — pd.read_json(..., typ='series') on the SEC's other ticker file gives 3 on every row, for cik_str, ticker and title, and str.get is what reaches into it by key.
Validating unstripped text. A trailing space is a character, and .str.len() counts it:
pd.Series(['NVDA ', 'AAPL', ' MSFT']).str.len()
0 5
1 4
2 5
dtype: int64
Two of those three now look like five-character tickers, so whatever rule you write next fires on the wrong rows in both directions. str.strip before you measure — and if the two counts differ, you have found a second problem.
Measuring characters when the limit is in bytes. Database columns, fixed-width files and most legacy systems budget bytes, and outside ASCII the two numbers part company:
pd.Series(['Nestlé S.A.', 'Ørsted A/S']).str.len()
0 11
1 10
dtype: int64
pd.Series(['Nestlé S.A.', 'Ørsted A/S']).str.encode('utf-8').str.len()
0 12
1 11
dtype: int64
Eleven characters, twelve bytes. If the receiving field is VARCHAR(11), your length check passed and your load will still fail.
Is it still slower than apply(len)?
In Bamboo Weekly #48 I called .str.len() on a column of lists "a bit sneaky," timed it, and admitted it was elegant rather than fast: about 9 ms against 6.9 ms for .apply(len). I re-ran that on Pandas 3.0.5 with PyArrow-backed strings, using the name column above repeated to 208,060 rows:
names = pd.concat([df['name']] * 20, ignore_index=True)
%timeit names.str.len() # 2.2 ms
%timeit names.apply(len) # 32.5 ms
A complete reversal — fifteen times faster, not slower. PyArrow computes the lengths in one pass over the string buffer and never builds a Python object. On lists nothing changed: 25.4 ms against 24.9 ms, because a list is still a Python object either way. The old warning is right for the case where I made it, and now wrong for text.
Where it shows up in Bamboo Weekly
12 Bamboo Weekly solutions use .str.len(), across 44 separate calls, and most of those are the .str.split().str.len() word count rather than a character count.
Bamboo Weekly #15: Eurovision is the simplest: df['lyrics'].str.split().str.len() gives the number of words in each song, plotted against the song's finishing place. The regression line comes out flat — wordier songs do not do better.
Bamboo Weekly #79: Cyber attacks is the filtering idiom you will reuse most: .loc[lambda s_: s_.str.len() >= 5] inside a split-explode-count chain, throwing away the short words before value_counts. That pattern appears in half the solutions on this list.
Bamboo Weekly #6: End of the humanities? is where I introduce the .str accessor with .str.len() as the example, on a column of degree names carrying footnote markers — a length anomaly of exactly the kind this page is about.
Practice it
Work through a str.len exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/str-len/
Go deeper
.str.len() travels with the rest of the small string methods — str.strip, str.lower, str.get and str.slice — which together make the first pass over a text column. str.split turns a character count into an element count, and explode is usually what follows it. Once the lengths have named the wrong rows, loc gets at them and value_counts counts what is left. And a length problem is often a type problem underneath, which is astype.
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.
Related methods
.str.strip()— when the wrong length turns out to be stray whitespace.str.get()— when you want the elements themselves rather than how many there are
See it on real data
Below are the 12 Bamboo Weekly exercises that use str.len on real-world data — try each one, then study the worked solution.
- Bamboo Weekly #182: Surveillance technology
- Bamboo Weekly #180: Movies
- Bamboo Weekly #159: State of the Union
- Bamboo Weekly #137: UN Security Council
- Bamboo Weekly #129: Tom Lehrer
- Bamboo Weekly #106: Flu season
- Bamboo Weekly #79: Cyber attacks
- Bamboo Weekly #74: UK elections
- Bamboo Weekly #48: Aviation accidents
- Bamboo Weekly #44: Global economics
- Bamboo Weekly #15: Eurovision
- Bamboo Weekly #6: End of the humanities?
Part of the Pandas Methods Index. See also practice by skill.