Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To filter a pandas DataFrame on several conditions, write each condition as its own boolean Series, wrap it in parentheses, and join the pieces with & (AND), | (OR), or ~ (NOT). Pass the combined mask inside df[...] or df.loc[...]. For example, df[(df["A"] > 2) & (df["B"] < 3)] keeps only the rows where column A is greater than 2 and column B is less than 3.

The rest of this guide explains why the parentheses matter, which operator to use for each logic case, how to select columns in the same step, and how to handle missing values, which are the most common reasons a multi-condition filter returns the wrong rows.

Why the Python and and or keywords fail on Series

Python’s and, or, and not keywords expect a single true or false value. A pandas column comparison returns a whole Series of True and False values, so Python cannot decide its truth value on its own. Using and between two Series typically raises a ValueError stating that the truth value of a Series is ambiguous. The element-wise operators &, |, and ~ are the ones pandas expects, as shown in the pandas indexing and selecting guide.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why every comparison needs parentheses

The & and | operators bind more tightly than comparison operators such as > and <. Without parentheses, df["A"] > 2 & df["B"] < 3 is not read as two separate conditions. Python instead evaluates a chained comparison that mixes the numbers and columns in an unintended order. Wrapping each comparison in its own parentheses forces the intended grouping:

good = df[(df["A"] > 2) & (df["B"] < 3)]
wrong = df[df["A"] > 2 & df["B"] < 3]   # unintended precedence

The wrong version often raises an error or produces a result you did not intend, so treat the missing parentheses as the first thing to check when a multi-condition filter behaves strangely.

Combining conditions with AND, OR, and NOT

The three operators cover every logical combination you need. The table below uses this small sample, which you can paste into a session to test each pattern:

import pandas as pd

df = pd.DataFrame({"A": [1, 3, 5, -2], "B": [2, 4, 1, 12]})
Goal Pattern Rows kept from the sample
Both conditions true (AND) df[(df["A"] > 2) & (df["B"] < 3)] Row 2 only (A = 5, B = 1)
Either condition true (OR) df[(df["A"] < 0) | (df["B"] > 10)] Row 3 only (A = -2, B = 12)
Condition is false (NOT) df[~(df["A"] > 2)] Rows 0 and 3 (A is 2 or less)

Each row in this table was checked by hand against the sample values above. With a real dataset, the same patterns apply, but the rows returned depend entirely on your data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choosing a filtering form

Several forms can express the same multi-condition filter. The right one depends on whether you also need to pick columns and how readable the expression needs to be.

Boolean indexing

Boolean indexing keeps the mask visible as a named variable, which makes it the best default for examples, reusable conditions, and longer Python expressions:

mask = (df["A"] > 2) & (df["B"] < 3)
filtered = df[mask]

Filtering rows and selecting columns with .loc

Use .loc when you want the row mask and a column selection in one step. Pass the mask first and the column list second:

result = df.loc[mask, ["A", "B"]]

.loc is label-aware, so a boolean Series is accepted as the row indexer as long as it aligns with the DataFrame index. The indexing guide notes that .iloc does not accept a boolean Series; a boolean NumPy array works with .iloc, so convert the mask with .to_numpy() if you must use position-based selection.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Using query for readable expressions

DataFrame.query() accepts a string expression, which can be easier to scan when the conditions are simple and column-based:

filtered = df.query("A > 2 and B < 3")

The string uses the keywords and, or, and not, not the operators &, |, and ~. To reference a Python variable inside the string, prefix it with @, for example df.query("A > @threshold").

Do not pass untrusted user input directly into query(). The pandas.DataFrame.query API reference warns that query expressions can run arbitrary code, so build such strings only from values you control, or use boolean indexing for user-supplied criteria.

Handling missing values in masks

Missing values change what a mask means, and they are the most common cause of a filter that silently drops rows. Work through these steps when a mask contains missing entries:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Check the dtype of the mask. A mask stored with the nullable boolean dtype can hold pd.NA in addition to True and False.
  2. Decide what a missing condition should mean for your task: drop the row, keep the row, or handle it as a separate case.
  3. Apply the decision explicitly. Per the pandas nullable Boolean data type guide, missing values in a boolean indexer are treated as False.
# Drop rows where the condition is missing (the default behavior)
kept = df[mask]

# Keep rows where the condition is missing
kept_including_unknown = df[mask.fillna(True)]

# Drop them explicitly, which makes the intent visible in code
dropped = df[mask.fillna(False)]

Choose the fill value from the meaning of your data rather than by habit. A missing age in a filter for “under 30” may reasonably be kept for manual review, while a missing price in a filter for “under 50” may reasonably be dropped.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Row filtering versus conditional values

Filtering removes rows that fail a condition. If you instead want to assign a label or value based on several ordered conditions and keep every row, use numpy.select. Its signature takes a list of conditions, a matching list of choices, and a default:

import numpy as np

df["group"] = np.select(
    [df["A"] > 4, df["A"] > 2],
    ["high", "medium"],
    default="low",
)

The first matching condition wins, so order the list from most specific to least specific. This pattern creates a new column and does not reduce the DataFrame.

Troubleshooting common symptoms

  • ValueError about the truth value of a Series: you combined masks with and, or, or not. Replace them with &, |, and ~.
  • Rows returned that do not match the logic: check each comparison is wrapped in parentheses before the & or | operator.
  • Fewer rows than expected: look for missing values in the mask. Nullable Boolean entries that are pd.NA are excluded unless you fill them.
  • Filter fails with .iloc: pass a NumPy boolean array rather than a boolean Series, or switch to .loc.

,
body_html_placeholder_removed”

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.