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.
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'.
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.
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.
| Flag | Meaning |
|---|---|
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 |
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.
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'].
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.