pandas str.contains: Filter Rows by Text in Python

By Dr. Zubair Khalid, DVM, MS, PhD ·

pandas str.contains: Filter Rows by Text in Python

If you want to filter a pandas DataFrame by text, str.contains in Python is the tool you reach for. It tests each string in a Series against a substring or regular expression and returns a boolean mask you can pass straight into df[...]. This article covers the syntax, the arguments that matter, a full worked example, and the errors that trip people up.

Quick Answer

  • df['col'].str.contains('text') returns a boolean Series, True where the pattern appears in the string.
  • Pass that mask into df[mask] to keep only the matching rows.
  • Set case=False to ignore capitalization. The default is case=True, so 'pro' and 'Pro' are different by default.
  • str.contains treats the pattern as a regular expression by default. Set regex=False to match literal text.
  • Missing values (NaN) produce NaN in the mask, which raises an error when used for indexing. Handle them with na=False.

Syntax

The method is called on a string Series, not on the DataFrame itself.

Series.str.contains(pat, case=True, flags=0, na=None, regex=True)
ArgumentRequired?Meaning
patYesThe substring or regular expression to search for.
caseNoIf True (default), the match is case sensitive. If False, case is ignored.
flagsNoRegex flags passed to the underlying engine, such as re.IGNORECASE.
naNoValue to use for missing entries. The default None leaves NaN in the result.
regexNoIf True (default), pat is treated as a regular expression. If False, it is matched literally.

The return value is a boolean Series aligned to the original index.

How It Works

str.contains walks each string element of the Series and asks whether the pattern appears anywhere inside it. Non-string elements such as numbers or missing values are not converted, they produce a missing value in the result. The result is a Series of True and False values with the same index as the input.

That boolean Series is a mask. When you write df[mask], pandas keeps the rows where the mask is True and drops the rest. The row order is preserved, and the index labels travel with the rows.

Two behaviors are worth remembering. First, the search is a substring search by default, so 'Pro' matches 'ProBook', 'Surface Pro 9', and 'iPad Pro 12' alike. Second, because regex=True is the default, characters like ., *, +, ?, (, ), [, ], ^, and $ carry special meaning. If your search text contains any of those and you want a literal match, pass regex=False.

The mask is a plain boolean Series, so it composes with other masks using & (and), | (or), and ~ (not). Wrap each condition in parentheses when you combine them.

Worked Example

The dataset is a small product catalog with 8 product names and prices.

nameprice
ProBook 450899
MacBook Air1099
ProDesk 600749
Chromebook 14329
ProLiant DL3802450
ThinkPad X11599
Surface Pro 9999
iPad Pro 121199

Build the DataFrame and apply the mask.

import pandas as pd
df = pd.DataFrame({
    "name": ["ProBook 450", "MacBook Air", "ProDesk 600", "Chromebook 14",
             "ProLiant DL380", "ThinkPad X1", "Surface Pro 9", "iPad Pro 12"],
    "price": [899.00, 1099.00, 749.00, 329.00, 2450.00, 1599.00, 999.00, 1199.00],
})
mask = df['name'].str.contains('Pro', case=False)
filtered = df[mask]
print(filtered)

Output:

             name   price
0     ProBook 450   899.0
2     ProDesk 600   749.0
4  ProLiant DL380  2450.0
6   Surface Pro 9   999.0
7     iPad Pro 12  1199.0

Step by step:

  1. The DataFrame has 8 rows and columns ['name', 'price'].
  2. str.contains('Pro', case=False) produces the mask [True, False, True, False, True, False, True, True].
  3. mask.sum() is 5, so 5 of 8 rows match, which is 62.5%.
  4. With case=True the count is still 5, because every match here already has a capital Pro.
  5. Using the regex r'^Pro' gives 3 matches, since only three names start with Pro.
  6. df[mask] returns the 5 rows: ProBook 450, ProDesk 600, ProLiant DL380, Surface Pro 9, and iPad Pro 12.

Notice that Surface Pro 9 and iPad Pro 12 match even though Pro is not at the start. That is the substring behavior in action. If you only want names that begin with Pro, anchor the pattern with ^.

More Examples

Case-sensitive search. The default is case=True.

df[df['name'].str.contains('pro')]  # 0 rows, lowercase 'pro' appears nowhere

Ignore case. Add case=False to catch every capitalization.

df[df['name'].str.contains('pro', case=False)]  # 5 rows

Anchor to the start with regex. The ^ character means "start of string."

df[df['name'].str.contains(r'^Pro')]  # 3 rows: ProBook, ProDesk, ProLiant

Match literal text with special characters. Turn off regex so + and . are treated as plain characters.

df[df['name'].str.contains('X1', regex=False)]  # 1 row: ThinkPad X1

Combine two conditions. Use & for AND and | for OR, with parentheses around each mask.

mask = df['name'].str.contains('Pro', case=False) & (df['price'] > 1000)
df[mask]  # ProLiant DL380 and iPad Pro 12

Handle missing values. Pass na=False so NaN entries become False instead of NaN.

df['name'].str.contains('Pro', case=False, na=False)

If you need to select rows and columns by label at the same time, pandas loc accepts the same boolean mask and lets you name the columns you want back.

Errors and How to Fix Them

ValueError: Cannot mask with non-boolean array containing NA / NaN values. This happens when the column has missing values and you did not set na. The mask contains NaN, and pandas cannot index with it. Fix it by passing na=False.

df[df['name'].str.contains('Pro', case=False, na=False)]

AttributeError: Can only use .str accessor with string values. The column is not a string dtype, often because it is numeric or mixed. Convert it first with df['col'].astype(str) before calling .str.contains.

re.error: nothing to repeat or similar regex errors. Your pattern contains regex metacharacters that are not valid on their own, such as a leading * or +. Escape them with a backslash or set regex=False for a literal match.

TypeError: bad operand type for unary ~: 'float'. You applied ~ to a mask that contains NaN. Set na=False so the mask is purely boolean before negating it.

Common Mistakes

  • Forgetting case=False. The default is case sensitive, so 'pro' will not match 'ProBook'. Add case=False when capitalization should not matter.
  • Leaving regex=True with special characters. A pattern like 'C++' or 'a.b' is interpreted as a regex. Pass regex=False for literal matching.
  • Ignoring NaN values. Missing entries produce NaN in the mask and break indexing. Always set na=False when the column may have gaps.
  • Using and / or instead of & / |. Python's and and or do not work element-wise on Series. Use & and | and wrap each condition in parentheses.
  • Calling str.contains on the DataFrame. The method lives on a Series. Write df['name'].str.contains(...), not df.str.contains(...).
  • Assuming str.contains matches whole strings. It matches substrings anywhere in the value. Anchor with ^ and $ if you need the full string to match.

Limitations

str.contains is a substring and regex test, not a fuzzy matcher. It will not find 'Macbook' when the data says 'MacBook' unless you normalize case first, and it will not handle typos, transpositions, or near-matches. For approximate matching you need a different approach, such as normalizing the text or using a string similarity library.

It also operates element by element, so it does not scale as well as vectorized numeric operations on very large frames. On millions of rows, a regex pattern can be noticeably slower than a literal one, and regex=False is the faster path when you do not need pattern matching. Finally, the mask reflects the data as stored. Leading or trailing whitespace, invisible characters, and inconsistent encodings can all cause a match to fail even when the text looks correct on screen.

Frequently Asked Questions

How do I filter a DataFrame by substring in pandas?

Call str.contains on the string column to get a boolean mask, then index the DataFrame with it. For example, df[df['name'].str.contains('Pro', case=False)] keeps every row whose name contains Pro in any capitalization. The mask preserves row order and index labels.

What is the difference between case=True and case=False?

case=True is the default and makes the match case sensitive, so 'Pro' and 'pro' are treated as different patterns. case=False ignores capitalization and matches either form. Use case=False when your data has inconsistent capitalization, which is common in scraped or user-entered text.

Does str.contains use regular expressions by default?

Yes. The regex argument defaults to True, so the pattern is compiled as a regular expression. That means ^, $, ., *, and other metacharacters have special meaning. Set regex=False when you want the pattern treated as plain literal text.

How do I match text at the start or end of a string?

Use regex anchors. r'^Pro' matches strings that begin with Pro, and r'12$' matches strings that end with 12. You can combine them, as in r'^Pro.*450$', to require both ends to match. Anchors only work while regex=True.

Why does str.contains raise a ValueError about NaN?

The mask contains NaN for missing entries, and pandas cannot use a mask with missing values for indexing. Pass na=False so missing entries become False and the mask stays purely boolean. This is the most common fix for that error.

If you work in spreadsheets too, the same idea of testing whether a cell contains text appears in Excel CONTAINS checks and in Excel IF formulas for cell text, and you can return matching rows with the Excel FILTER function.

References

This article draws on the standard references listed under Further Reading.

Further Reading

Related Articles