Truthiness of containers and falsy values

Updated 4 min read source
On this page10
  1. The gotcha
  2. Minimal repro
  3. The full list of falsy values
  4. How bool() decides
  5. The if items vs if items is not None bug
  6. DataFrames and arrays — bool() is forbidden
  7. “Truthy short-circuit” patterns
  8. Boolean operators don’t return bool
  9. Comparing with True / False directly
  10. Interview angle

Truthiness of containers and falsy values

The gotcha

bool([]) is False. bool([0]) is True. bool([False]) is True. The container’s truthiness depends on whether it’s empty, not on what’s inside.

Minimal repro

python
bool([])           # False  — empty list
bool([0])          # True   — one element, even though that element is falsy
bool([False])      # True   — same
bool([None])       # True   — same
bool([[]])         # True   — list containing an empty list

bool({})           # False  — empty dict
bool({0: 0})       # True   — non-empty
bool(set())        # False
bool({0})          # True

bool("")           # False
bool("0")          # True   — non-empty string, even if it looks like zero
bool(" ")          # True   — whitespace is non-empty

bool(())           # False  — empty tuple
bool((0,))         # True

The full list of falsy values

In Python, only these are falsy:

  • None
  • False
  • Numeric zeros: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  • Empty sequences: "", (), [], range(0), bytes()
  • Empty mappings: {}
  • Empty sets: set(), frozenset()
  • Custom objects whose __bool__ returns False, or whose __len__ returns 0 (with no __bool__)

Everything else is truthy.

How bool() decides

For a custom object:

  1. Call __bool__() if defined → must return bool.
  2. Else call __len__() if defined → 0 is False, anything else True.
  3. Else default to True (every object is truthy by default).
python
class Empty:
    def __bool__(self):
        return False

bool(Empty())    # False

class Box:
    def __init__(self, items): self.items = items
    def __len__(self): return len(self.items)

bool(Box([]))    # False  — falls through __bool__, uses __len__
bool(Box([0]))   # True   — __len__ returns 1

The if items vs if items is not None bug

The classic bug:

python
def get_users(filter=None):
    # falsy if {} or [] passed in
    if filter:
        return User.query.filter_by(**filter).all()
    return User.query.all()

# falls through — but caller probably meant "no filters"
get_users(filter={})

If callers might pass an empty dict or list to mean “this is the value, just empty,” use explicit None checks:

python
def get_users(filter=None):
    # treats {} as a real value
    if filter is not None:
        return User.query.filter_by(**filter).all()
    return User.query.all()

The same trap with optional integer arguments:

python
def paginate(items, limit=None):
    # limit=0 is falsy → no limit applied
    if limit:
        items = items[:limit]

limit=0 should mean “return nothing.” if limit: treats it as “no limit set.” Use if limit is not None:.

DataFrames and arrays — bool() is forbidden

python
import numpy as np
arr = np.array([1, 2, 3])
# ValueError: ambiguous
if arr:
    ...

NumPy and pandas raise on truthiness of multi-element arrays — which element should determine truthiness? Use arr.any(), arr.all(), or len(arr) > 0.

Pandas DataFrames same: if df: raises. Use df.empty.

“Truthy short-circuit” patterns

python
name = user_input or "default"          # uses "default" if user_input is "" or None
items = config.get("items") or []       # treats missing OR empty list the same way

These are idiomatic but conflate “missing” with “empty/zero.” Use them when the conflation is intentional.

python
# Intentional: any falsy value gets replaced
return user.display_name or user.email or "anonymous"

# Bug-prone: 0 quantity becomes "no quantity"
# qty=0 → silently becomes 1
quantity = parsed.get("qty") or 1

For the bug-prone case, use dict.get with a default, or check explicitly.

Boolean operators don’t return bool

and and or return one of their operands, not True/False:

python
1 or 2          # 1   — first truthy
0 or 2          # 2   — first truthy
1 and 2         # 2   — last truthy when all truthy
0 and 2         # 0   — first falsy
"" or "a"       # "a"
[] or [0]       # [0]

Only not returns a bool. This is what makes x or default an idiom — but also why subtle bugs hide.

Comparing with True / False directly

python
if response == True:     # almost never what you want

True == 1 is True (because bool is a subclass of int), so response == True matches both True and 1. Use if response: for truthiness, if response is True: for identity.

The == against True / False literal is also flagged by linters (E712 in pycodestyle/ruff).

Interview angle 4

  • Q: “Is bool([]) true or false? What about bool([0])?” — False, True. Container truthiness is emptiness.
  • Q: “When would if x: give the wrong answer?” — when 0, "", [], {} are valid values distinct from “missing.”
  • Follow-up: “How does Python decide truthiness for a custom class?” — __bool__ first, then __len__, default True.
  • Follow-up: “What does [] or [0] return?” — [0]. or returns the first truthy operand, not a bool.

See bool is a subclass of int, dict.get(k, default) and falsy values, Floats and equality.