# 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.

```python
Series.str.contains(pat, case=True, flags=0, na=None, regex=True)
```

| Argument | Required? | Meaning |
|---|---|---|
| `pat` | Yes | The substring or regular expression to search for. |
| `case` | No | If `True` (default), the match is case sensitive. If `False`, case is ignored. |
| `flags` | No | Regex flags passed to the underlying engine, such as `re.IGNORECASE`. |
| `na` | No | Value to use for missing entries. The default `None` leaves `NaN` in the result. |
| `regex` | No | If `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.

| name | price |
|---|---|
| ProBook 450 | 899 |
| MacBook Air | 1099 |
| ProDesk 600 | 749 |
| Chromebook 14 | 329 |
| ProLiant DL380 | 2450 |
| ThinkPad X1 | 1599 |
| Surface Pro 9 | 999 |
| iPad Pro 12 | 1199 |

Build the DataFrame and apply the mask.

```python
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`.

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

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

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

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

```python
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.

```python
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.

```python
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`.

```python
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](/blog/data-analysis/pandas-loc-select-rows-columns) 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`.

```python
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](/blog/data-analysis/excel-contains-check-cell-text) and in [Excel IF formulas for cell text](/blog/data-analysis/excel-if-cell-contains-text), and you can return matching rows with the [Excel FILTER function](/blog/data-analysis/excel-filter-function-syntax-examples).

## References

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

## Further Reading

- [Harris CR, Millman KJ, van der Walt SJ et al. (2020). Array programming with NumPy. Nature](https://doi.org/10.1038/s41586-020-2649-2)
- [McKinney W (2010). Data Structures for Statistical Computing in Python. Proceedings of the Python in Science Conference](https://doi.org/10.25080/majora-92bf1922-00a)
- [The Python Tutorial](https://docs.python.org/3/tutorial/index.html)
- [Wilson G, Bryan J, Cranston K et al. (2017). Good enough practices in scientific computing. PLOS Computational Biology](https://doi.org/10.1371/journal.pcbi.1005510)
- [Virtanen P, Gommers R, Oliphant TE et al. (2020). SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods](https://doi.org/10.1038/s41592-019-0686-2)
- [pandas User Guide](https://pandas.pydata.org/docs/user_guide/index.html)

## Related Articles

- [Excel CONTAINS: How to Check If a Cell Contains Text](/blog/data-analysis/excel-contains-check-cell-text)
- [Excel IF Cell Contains Text: Formula Examples and Wildcards](/blog/data-analysis/excel-if-cell-contains-text)
- [Pandas loc: How to Select Rows and Columns by Label](/blog/data-analysis/pandas-loc-select-rows-columns)
- [Excel FILTER Function: Syntax and Examples](/blog/data-analysis/excel-filter-function-syntax-examples)