Truthiness and Falsy Values

Python lets you write if x: for any object, not just booleans — every value has a truth value, obtained by calling bool() on it. This is concise and Pythonic, and it is also the source of a whole family of quiet bugs, because an empty list, an empty string, the integer 0, the float 0.0, and None are all falsy and therefore indistinguishable to a bare if x:. The official rule is exact: “By default, an object is considered true unless its class defines either a __bool__() method that returns False or a __len__() method that returns zero” (Python stdtypes — Truth Value Testing). The trap is that if x: conflates “x is missing” with “x is present but empty/zero/false.” When those two cases must be handled differently — and they usually must — if x: is a latent bug and if x is not None: is the fix. The dispatch mechanism (__bool__ then __len__, and the singletons) is covered in None True False and the Singletons and Dunder Methods and the Data Model; this note is about the trap, its symptoms, and the fixes.

Mental Model — Two Questions That if x: Smashes Together

The core confusion is that programmers reach for if x: when they mean one of two genuinely different questions:

  1. “Does x exist / was a value supplied?” — a presence check. The right tool is x is not None (or x is _SENTINEL).
  2. “Is x non-empty / non-zero / true?” — a content check. The right tool is if x: (or the explicit len(x) > 0, x != 0).

if x: only answers question 2, but people use it for question 1 — and it usually works, because most “missing” values are None (falsy) and most “present” values are non-empty (truthy). It breaks precisely at the overlap: when a present, valid value is also falsy — an empty list someone legitimately passed, a deliberate 0, an empty string that means “the user cleared this field.” Those are valid data, but if x: treats them identically to “absent.”

flowchart TD
    IF["if x:  -->  bool(x)"] --> B{"type(x) defines<br/>__bool__?"}
    B -- yes --> CALLB["call __bool__()<br/>(must return True/False,<br/>else TypeError)"]
    B -- no --> L{"type(x) defines<br/>__len__?"}
    L -- yes --> CALLL["call __len__()<br/>true iff result != 0"]
    L -- no --> T["considered True<br/>(default for any object)"]
    CALLB --> R["truth value"]
    CALLL --> R
    T --> R
    R --> TRAP["FALSY set collapses<br/>None / 0 / '' / [] / {} ...<br/>into one indistinguishable<br/>'false' — the trap"]

Diagram: the resolution order the interpreter uses to compute bool(x) (__bool__ wins; else __len__; else default-true), and the punchline — many distinct, valid values land in the single “falsy” bucket, which is exactly why if x: cannot tell “absent” from “present-but-empty.” The insight: choose your conditional based on which of those two questions you are actually asking.

The Full Set of Falsy Values

Per the Truth Value Testing docs, the built-in objects considered false are, exactly:

  • The false constants: None and False.
  • Zero of any numeric type: 0, 0.0, 0j (complex zero), Decimal(0), Fraction(0, 1).
  • Empty sequences and collections: '' (empty string), () (empty tuple), [] (empty list), {} (empty dict), set() (empty set), range(0) (empty range).

Beyond the built-ins, any object is falsy if its class defines __bool__() returning False, or (lacking __bool__) __len__() returning 0. Everything else — including any object that defines neither method — is truthy. Verified live on CPython 3.14.5:

>>> [bool(v) for v in (None, False, 0, 0.0, 0j, "", [], {}, (), set(), range(0))]
[False, False, False, False, False, False, False, False, False, False, False]
>>> bool(frozenset())     # empty frozenset
False
>>> bool(range(1))        # non-empty range -> True
True
>>> bool(0.0), bool(-0.0) # both zeros are falsy
(False, False)

A few that surprise people because they are truthy: "0" (a non-empty string — its characters are “0”, but it’s non-empty, so True), "False" (non-empty string, True), [[]] (a list containing one empty list — outer list is non-empty, True), (0,) (a one-element tuple, True), and " " (a single space, True). The truth value depends on emptiness/zeroness of the object itself, never on what a human reads in it.

How the Interpreter Resolves a Truth Value

The compiler turns if x: into a TO_BOOL bytecode (added in CPython 3.13, per the dis docs) followed by a conditional jump. Disassembling on 3.14.5:

>>> import dis
>>> def g(x):
...     if x:
...         return 1
...     return 0
>>> dis.dis(g)
  LOAD_FAST_BORROW         0 (x)
  TO_BOOL
  POP_JUMP_IF_FALSE        ...
  ...

TO_BOOL implements STACK[-1] = bool(STACK[-1]) — it replaces the top-of-stack value with its boolean. (Before 3.13 there was no dedicated opcode; the conditional jumps did the conversion inline. The 3.13 TO_BOOL exists partly so the specializing interpreter can have a fast path — e.g. when it observes the operand is already a bool, it specializes to TO_BOOL_BOOL, a near-no-op.) At the C level, bool(x) resolves via the type’s slots in this order, exactly as the data model specifies:

  1. __bool__. “Called to implement truth value testing and the built-in operation bool(); should return False or True” (datamodel, object.__bool__). It is consulted first. It must return an actual bool — returning anything else (even a truthy int) raises TypeError. Verified:

    >>> class Bad:
    ...     def __bool__(self): return 1
    >>> bool(Bad())
    TypeError: __bool__ should return bool, returned int
  2. __len__ (fallback). “When this method [__bool__] is not defined, __len__() is called, if it is defined, and the object is considered true if its result is nonzero” (datamodel). So a container with no __bool__ is falsy exactly when len(x) == 0. This is why [], {}, "", set(), range(0) are falsy — their __len__ returns 0.

  3. Default: true. “If a class defines neither __len__() nor __bool__(), all its instances are considered true.” A plain object(), a function, a module, an open file — all truthy by default.

Note the precedence: when a class defines both, __bool__ wins outright and __len__ is ignored for truth testing. Verified:

>>> class B:
...     def __bool__(self): return False
...     def __len__(self): return 10        # ignored for truthiness
>>> bool(B())
False

This precedence matters for a real class like a “lazy/optional container” that is non-empty but you want to treat as falsy until loaded — defining __bool__ lets you override the len-based default. The full slot/dispatch story (nb_bool, sq_length/mp_length) is in Dunder Methods and the Data Model.

The Canonical Traps

Trap 1 — if x: when you mean if x is not None: (the default-argument classic)

The single most common version of this bug is the mutable-default-replacement idiom done wrong:

def append_to(item, target=None):
    if not target:            # BUG: empty list is falsy, gets replaced!
        target = []
    target.append(item)
    return target

The intent is “if the caller didn’t pass a target, start a fresh list.” But if not target: is true for None and for an empty list the caller deliberately passed. So:

>>> existing = []
>>> append_to(1, existing)
[1]
>>> existing            # the caller's list was NOT used — a new one was made
[]

The caller passed a valid (empty) list expecting it to be appended to; not target saw it as falsy and silently discarded it. The fix is the presence check:

def append_to(item, target=None):
    if target is None:        # ONLY None means "not supplied"
        target = []
    target.append(item)
    return target

is None distinguishes “absent” (the sentinel None) from “present but empty” ([]). This is the same is-for-singletons rule as Integer and String Identity Surprises: None is a guaranteed singleton, so is None is the exact, un-overridable test. (The related “why not target=[] directly” trap — the mutable default itself — is Mutable Default Arguments.)

Trap 2 — if not x: swallowing legitimate zero / empty / empty-string

Anywhere a falsy-but-valid value is meaningful, if not x: and if x: quietly mishandle it:

def set_volume(level=None):
    if not level:             # BUG: level=0 (mute) is falsy
        level = DEFAULT_VOLUME
    ...
>>> set_volume(0)             # user asked for mute...
>>> # ...but got DEFAULT_VOLUME instead
 
config = {"retries": 0}
timeout = config.get("timeout") or 30   # BUG if timeout is legitimately 0
count = parse_count() or 10             # BUG: a real 0 becomes 10
name = form.get("name") or "Anonymous"  # BUG: "" (cleared field) becomes "Anonymous"

The or-default idiom (value or fallback) is the most insidious because it reads so naturally, yet it triggers on every falsy value, not just None. A volume of 0, a retry count of 0, an empty-string name the user intentionally cleared — all get clobbered by the fallback. The fixes are explicit:

level = DEFAULT_VOLUME if level is None else level
timeout = config["timeout"] if "timeout" in config else 30
name = form["name"] if "name" in form else "Anonymous"   # "" stays ""

When the only invalid sentinel is None, prefer x if x is not None else default. When you genuinely want “non-empty,” say it: if len(x) > 0: or if x != 0: documents intent far better than if x:.

Trap 3 — NumPy / pandas raise on bool() of a multi-element array

For a multi-element NumPy array, bool() is ambiguous — should an array [True, False] be true? NumPy refuses to guess and raises:

>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> if a:                 # implicit bool(a)
...     ...
ValueError: The truth value of an array with more than one element is
ambiguous. Use a.any() or a.all()

Uncertain (2026-06-01)

Verify: the exact wording of NumPy’s ambiguous-bool() ValueError (e.g. “The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()”). Reason: NumPy is a third-party package and is not installed in this environment, so the message text could not be reproduced live; it is taken from NumPy’s documented ndarray.__bool__ behavior. The principle — a library can make bool() raise, so if arr: is unsafe for arrays/Series — is solid and not the uncertain part. To resolve: run bool(np.array([1,2,3])) on a box with NumPy and confirm the message/version (pandas raises an analogous “The truth value of a Series is ambiguous”). This depends on an external package’s exact string, so it stays flagged. uncertain

The lesson generalizes: a class’s __bool__ can raise, in which case (per the docs) “the exception is propagated and the object does not have a truth value.” NotImplemented is the standard-library example — and in 3.14 bool(NotImplemented) raises TypeError (it was a DeprecationWarning since 3.9). So if x: is not even guaranteed to return — for array-like objects you must use the element-wise reducers a.any() / a.all(), or check shape/emptiness explicitly with a.size / len(a).

The Fixes, Summarized

The discipline is: match the test to the question.

  • Presence (“was a value supplied?”)if x is None: / if x is not None:. The only correct test when None is your “missing” sentinel, because it doesn’t false-positive on 0, "", [].
  • Distinct sentinel_MISSING = object(); if x is _MISSING:. Use when None is itself a valid value, so you need a marker that callers can’t accidentally produce.
  • Emptiness (“non-empty container?”)if len(x) > 0: (or if x: only when you’ve confirmed every falsy value is genuinely “do nothing”). Explicit len documents intent and dodges the array-ValueError.
  • Non-zero (“is this number set?”)if x != 0: (or if x is not None and x != 0:). Never collapse “zero” and “absent.”
  • Array-likea.any() / a.all() / a.size, never if a:.

The unifying rule mirrors the identity note: reach for the most specific test. if x: is the least specific — it answers “is this object truthy?” and almost nothing else is the actual question. Reserve it for the cases where you truly mean “skip on any empty/zero/false,” which is rarer than it feels.

Common Misunderstandings

  • if x is not None: and if x: are the same when x can’t be 0/empty.” Only if you can prove x is never 0/""/[]/{}. The day a refactor lets a 0 through, if x: breaks silently and if x is not None: keeps working. Prefer the one that stays correct under change.
  • x == None works fine.” It usually does, but is None is correct: None is a singleton (identity is exact), and == can be overridden by a malicious or buggy __eq__, while is cannot be intercepted. Same reasoning as Integer and String Identity Surprises.
  • bool(x) always succeeds.” False — __bool__ can raise (NumPy arrays, NotImplemented), so if x: can raise. Don’t assume a truth test is side-effect-free.
  • “Empty dict/set/str are different falsy values, so I can tell them apart in if.” No — if x: collapses all of them to “false.” If you need to distinguish “absent” from “present-but-empty,” if x: is structurally the wrong tool.

See Also