Truthiness of containers and falsy values

4 min read source

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

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).
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:

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

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

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

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

The same trap with optional integer arguments:

def paginate(items, limit=None):
    if limit:                           # limit=0 is falsy → no limit applied
        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

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

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

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.

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

# Bug-prone: 0 quantity becomes "no quantity"
quantity = parsed.get("qty") or 1       # qty=0 → silently becomes 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:

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

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

  • 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 04_bool_is_int_subclass.md, 16_dict_get_falsy_default.md, 17_floats_and_equality.md.