PythonMastery
reference 3 min read · lesson 59 of 60 in Errors

ValueError: The truth value of a Series is ambiguous

1 · The lesson

read

What this error means

Something asked a pandas Series for a single True or False, and a Series of many values can't give one. Python's and, or, not, if and while all need exactly one boolean. A Series like [True, False, True] could mean "yes" (some are true) or "no" (not all are), and pandas refuses to pick. The same thing happens with a DataFrame and with a NumPy array — only the wording changes.

When you see it

Filtering with and:

python
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    big_uk = orders[(orders["total"] > 100) and (orders["country"] == "UK")]
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../pandas/core/generic.py", line 1577, in __nonzero__
    raise ValueError(
    ...<2 lines>...
    )
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

The NumPy version, from an if on an array:

python
Traceback (most recent call last):
  File "main.py", line 4, in <module>
    if readings > 30:
       ^^^^^^^^^^^^^
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

On a whole DataFrame it reads The truth value of a DataFrame is ambiguous.

Why it happens

A comparison on a Series doesn't return one boolean. It returns a Series of booleans, one per row:

python
import pandas as pd

orders = pd.DataFrame({
    "customer": ["Ada", "Grace", "Linus", "Hedy"],
    "total":    [250.0, 40.0, 180.0, 95.0],
    "country":  ["UK", "UK", "FI", "AT"],
})
print(orders["total"] > 100)        # 0 True, 1 False, 2 True, 3 False

and, or and not are Python keywords, not operators that pandas can redefine. To decide what a and b means, Python calls bool(a) — and bool() of a four-row Series is exactly the question pandas won't answer:

python
import pandas as pd

orders = pd.DataFrame({
    "customer": ["Ada", "Grace", "Linus", "Hedy"],
    "total":    [250.0, 40.0, 180.0, 95.0],
    "country":  ["UK", "UK", "FI", "AT"],
})
big_uk = orders[(orders["total"] > 100) and (orders["country"] == "UK")]   # ValueError

if orders["total"] > 100: fails for the same reason, and so does if df: when you meant "is this DataFrame empty?".

There's a second trap that produces the same error even after you switch to &. In Python, & binds more tightly than > and >=. So this:

python
import pandas as pd

orders = pd.DataFrame({
    "customer": ["Ada", "Grace", "Linus", "Hedy"],
    "total":    [250.0, 40.0, 180.0, 95.0],
    "qty":      [3, 1, 2, 1],
})
orders[orders["total"] > 100 & orders["qty"] >= 2]     # ValueError, despite the &

is read as orders["total"] > (100 & orders["qty"]) >= 2 — a chained comparison, which Python evaluates with a hidden and. Hence the error. The brackets around each condition aren't style; they're required.

How to fix it

Element-wise logic: & for and, | for or, ~ for not — and wrap every condition in brackets.

python
import pandas as pd

orders = pd.DataFrame({
    "customer": ["Ada", "Grace", "Linus", "Hedy"],
    "total":    [250.0, 40.0, 180.0, 95.0],
    "country":  ["UK", "UK", "FI", "AT"],
})

big_uk = orders[(orders["total"] > 100) & (orders["country"] == "UK")]
print(big_uk)                   # Ada only

big_or_uk = orders[(orders["total"] > 100) | (orders["country"] == "UK")]
print(big_or_uk["customer"].tolist())   # ['Ada', 'Grace', 'Linus']

not_uk = orders[~(orders["country"] == "UK")]
print(not_uk["customer"].tolist())      # ['Linus', 'Hedy']

Once there are three or four conditions, name them. It reads better and the brackets look after themselves:

python
import pandas as pd

orders = pd.DataFrame({
    "customer": ["Ada", "Grace", "Linus", "Hedy"],
    "total":    [250.0, 40.0, 180.0, 95.0],
    "country":  ["UK", "UK", "FI", "AT"],
})

is_big = orders["total"] > 100
in_europe = orders["country"].isin(["UK", "FI", "AT"])
print(orders[is_big & in_europe])

For a range, orders["total"].between(50, 200) beats two comparisons joined with &.

A single yes/no about the whole Series: say which question you mean.

python
import pandas as pd

totals = pd.Series([250.0, 40.0, 180.0, 95.0])

if (totals > 200).any():
    print("at least one order over 200")
if (totals > 0).all():
    print("no zero or negative totals")

refunds = totals[totals < 0]
if refunds.empty:
    print("no refunds today")

Use .empty rather than if df: or len(df) == 0 when you're checking whether a filter matched anything. And if you know the Series has exactly one value — say, one row matched by ID — pull it out with .item(), which raises if there isn't exactly one.

Ignore the a.bool() the message suggests. It has been deprecated since pandas 2.1 (pandas 2.3 prints a FutureWarning when you call it) and is on its way out.

NumPy arrays: the same fixes. &, |, ~ with brackets for element-wise logic; .any() or .all() for a single answer:

python
import numpy as np

readings = np.array([21.5, 33.0, 28.4, 35.2])   # temperatures, °C
hot = readings[(readings > 30) & (readings < 35)]
print(hot)                      # [33.]

if (readings > 30).any():
    print("at least one reading over 30 °C")

When you'd actually see this in real code

  • Filtering a sales DataFrame with and because it reads like English — almost everyone does this once.
  • if df["status"] == "shipped": inside a function that was written for one row and later handed a whole column.
  • df.apply(...) with a function that does if row_value > threshold — fine per row, but the same function called on the full column raises this.
  • Checking if not df: after loading a CSV to see if it was empty.
  • A sensor or price check written for one reading, if price > limit: alert(), later fed a whole NumPy array of prices.

See Also

Found this useful? Share it with a friend who’s learning Python.