pandas filter() — keep columns and rows by name, substring, or regex

filter() selects by label, not by value — it's the SQL LIKE of pandas column selection.

You've been writing df[[c for c in df.columns if 'eur' in c]] for years — pandas shipped the one-liner in 2009.

What it does

df.filter() keeps columns or rows whose LABELS match. Three mutually exclusive matchers: items (exact label list), like (substring), regex (regular expression). By default it works on the column axis; pass axis=0 to filter row labels instead. Nothing is removed from the source — you get a new DataFrame back. Missing labels in items are silently skipped rather than raising a KeyError.

Why it matters

Wide tables with generated column names are everywhere: sensor_2024_01, sensor_2024_02, revenue_eur, revenue_usd, 47 one-hot dummies. df.filter(regex=...) grabs exactly the family you want in one line, with no list comprehension. It also searches MultiIndex levels — the only column selector that understands hierarchical names natively — and it composes cleanly in method chains, where bracket indexing does not.

Examples

You got a sales report and only need two of its four columns before sending it on. Pick them by exact name with items:
   order_id  revenue_eur
0      1001        144.0
1      1002         55.3
2      1003        384.0
3      1004         27.9

items takes any list-like of exact labels. Try to keep a column that doesn't exist (say "discount") and filter just skips it — no KeyError, no warning. Verified: df.filter(items=["order_id", "discount"]) returns just order_id.

All metric columns share a prefix or suffix. like does substring matching, regex does full pattern matching — both in one call. df.filter(like="revenue") keeps just revenue_eur; the regex form below grabs the id and revenue family at once:
   order_id  revenue_eur
0      1001        144.0
1      1002         55.3
2      1003        384.0
3      1004         27.9

That's df.filter(regex="^(order|rev)"). regex uses re.search, not re.match — "eur" alone would also match anything containing those letters mid-name. Anchor with ^ or $ when the pattern should sit at an edge.

Rows are labels too. On a quarterly city table, grab one row by name with axis=0:
         q1   q2
Berlin  810  902

That's df.filter(like="Berlin", axis=0); df.filter(regex="^V", axis=0) similarly keeps only Vienna (both verified). Bonus, also verified: on a MultiIndex frame, df.filter(like="revenue", axis=1) keeps every ('berlin','revenue') and ('vienna','revenue') column at once — and df.filter(items=[("berlin","revenue")]) selects one tuple exactly.

Flags

FlagMeaning
items=[...]exact labels to keep; missing ones are silently ignored
like="revenue"keep labels containing this substring
regex="^rev"keep labels where re.search matches (not re.match)
axis=0 / axis=1which axis to match on: 1/'columns' (default) filters columns, 0/'index' filters rows
mutually exclusiveitems, like, regex: pass exactly one; passing none raises TypeError
lenient itemsdf.filter(items=["a", "zzz"]) never raises KeyError — unlike df["zzz"]
case sensitivitylike is a plain Python substring check; use regex="(?i)eur" for case-insensitive

Older than most of its users

The first commit adding DataFrame.filter landed on December 31, 2009, when pandas still lived on Google Code under Wes McKinney's original SVN import — it shipped in the 0.3.0 era as a columns-only helper (items, like, regex), with no axis argument at all.

Generalized in 0.13

In January 2014, 0.13.0 moved filter up into NDFrame (pandas/core/generic.py) so Series, DataFrame, and Panel all shared one implementation, and added the axis parameter — that's the moment rows became filterable too.

Under the hood: one codepath, three predicates

After PR #52941 (April 2023, pandas 2.1), all three match modes funnel into a single fast path over the axis Index. items does a direct isin() lookup, like builds a boolean via a Python-level substring test, and regex compiles once with re.compile then runs search over the labels — labels are matched as strings, so a numeric column named 2024 matches regex="^2" through its str form. The result is a take-based selection, so filter() never copies more than the selected blocks.

Fun facts

Pros

Cons

Takeaways