pandas isin() — filter rows by membership in a list

SQL's WHERE … IN, translated to pandas: one list in, one boolean mask out.

Your report covers six regions but leadership only funds two — and someone is about to write four lines of OR.

What it does

isin() asks one question of every element: are you in this list? df[df['region'].isin(['east', 'south'])] keeps exactly the rows whose region is 'east' or 'south', in one call. The argument is any list-like — list, tuple, set, even another Series or Index — and the return is a boolean mask the same length as the caller, which you feed straight back into the bracket operator. Negation is the tilde: ~df['region'].isin(...) is your 'not in'.

Why it matters

The alternative is (df.a == 'east') | (df.a == 'south') | (df.a == 'west'), and it gets worse with every candidate you add. isin() replaces that chain with one expression that survives editing — add a city to the list and nothing else changes. It is also faster: I measured ~6x over the chained-OR idiom on 100k rows in pandas 3.0.3, because pandas dispatches to a C-level membership routine instead of building one temporary comparison array per candidate.

Examples

import pandas as pd

df = pd.DataFrame({
    "city": ["Berlin", "Munich", "Hamburg", "Cologne", "Dresden", "Leipzig"],
    "region": ["east", "south", "north", "west", "east", "east"],
    "sales": [4200, 5100, 3800, 6400, 2900, 3300],
})
df[df["region"].isin(["east", "south"])]
      city region  sales
0   Berlin   east   4200
1   Munich  south   5100
4  Dresden   east   2900
5  Leipzig   east   3300

The classic report filter: keep only the rows whose region is one of my candidates. One line, no OR chains.

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005],
    "status": ["shipped", "pending", "shipped", "cancelled", "processing"],
})
orders[~orders["status"].isin(["shipped", "cancelled"])]
   order_id      status
1      1002     pending
4      1005  processing

~ flips the mask — this is the entire 'not in' story, no separate method to remember.

top_cities = df.nlargest(3, "sales")["city"]
df[df["city"].isin(top_cities)]
      city  sales
0   Berlin   4200
1   Munich   5100
3  Cologne   6400

isin() accepts a Series, so filters compose: rank first, then keep every row matching the winners.

grades = pd.DataFrame({"math": [90, 55], "art": [72, 88]})
grades.isin({"math": [90, 91], "art": [88]})
    math    art
0   True  False
1  False   True

Call it on the whole DataFrame with a dict to test each column against its own candidate list.

Flags

FlagMeaning
s.isin(['a', 'b'])the core move: one list-like of candidates, one boolean mask out
~s.isin(...)negate the mask for 'not in' — no separate API
df.isin({'math': [90], 'art': [88]})dict form on a DataFrame: per-column candidate lists, cell-wise booleans
df.isin(other_df)DataFrame argument: element-wise match against an equally-shaped frame
s.isin({'a', 'b'})any list-like works — set, tuple, pd.Series, pd.Index
s.isin(matcher_series)only the matcher's index values count, its data is ignored — pass .index or a plain list
df.query('region in @regions')sibling idiom: same result as an expression string, SQL-flavored

Three versions, three years

Index.isin() shipped first in 0.7.0 (February 2012), Series.isin() followed in 0.11.0 (April 2013, GH 289), and DataFrame.isin() completed the set in 0.13.0 (January 2014, GH 4211) — the very release that also introduced query(). The membership idea was considered so core that it was rolled out object type by object type over two years.

The string guard (also 0.13)

In that same 0.13.0 changelog: isin() raises TypeError when passed a bare string, with the message telling you to wrap it in a one-element list. The design note: s.isin('ab') would silently mean 'match the characters a and b', and the team decided surprising-you-per-character was worse than making you type ['ab'].

One method, two algorithms

pandas.core.algorithms.isin() inspects your inputs before choosing a strategy. For non-object dtypes with a candidate list of ≤26 values it calls numpy's sorted-array np.isin — faster than hashing when the list is tiny. Beyond that (or with >1,000,000 rows, a threshold added for pd.isin in 1.2.0, GH 36611) it builds a hash table via htable.ismember for O(1) lookups. Both paths are C. Before any of that, the function rejects non-list-like values with the TypeError guard from 0.13 — so the error you get for isin('east') is enforced at the C boundary, not by accident.

Fun facts

Pros

Cons

Takeaways