Truthiness and Falsy Values
Python lets you write
if x:for any object, not just booleans — every value has a truth value, obtained by callingbool()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 integer0, the float0.0, andNoneare all falsy and therefore indistinguishable to a bareif x:. The official rule is exact: “By default, an object is considered true unless its class defines either a__bool__()method that returnsFalseor a__len__()method that returns zero” (Python stdtypes — Truth Value Testing). The trap is thatif 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 andif 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:
- “Does
xexist / was a value supplied?” — a presence check. The right tool isx is not None(orx is _SENTINEL). - “Is
xnon-empty / non-zero / true?” — a content check. The right tool isif x:(or the explicitlen(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:
NoneandFalse. - 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:
-
__bool__. “Called to implement truth value testing and the built-in operationbool(); should returnFalseorTrue” (datamodel,object.__bool__). It is consulted first. It must return an actualbool— returning anything else (even a truthyint) raisesTypeError. Verified:>>> class Bad: ... def __bool__(self): return 1 >>> bool(Bad()) TypeError: __bool__ should return bool, returned int -
__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 whenlen(x) == 0. This is why[],{},"",set(),range(0)are falsy — their__len__returns 0. -
Default: true. “If a class defines neither
__len__()nor__bool__(), all its instances are considered true.” A plainobject(), 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())
FalseThis 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 targetThe 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 targetis 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 documentedndarray.__bool__behavior. The principle — a library can makebool()raise, soif arr:is unsafe for arrays/Series — is solid and not the uncertain part. To resolve: runbool(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 whenNoneis your “missing” sentinel, because it doesn’t false-positive on0,"",[]. - Distinct sentinel →
_MISSING = object();if x is _MISSING:. Use whenNoneis itself a valid value, so you need a marker that callers can’t accidentally produce. - Emptiness (“non-empty container?”) →
if len(x) > 0:(orif x:only when you’ve confirmed every falsy value is genuinely “do nothing”). Explicitlendocuments intent and dodges the array-ValueError. - Non-zero (“is this number set?”) →
if x != 0:(orif x is not None and x != 0:). Never collapse “zero” and “absent.” - Array-like →
a.any()/a.all()/a.size, neverif 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:andif x:are the same whenxcan’t be 0/empty.” Only if you can provexis never0/""/[]/{}. The day a refactor lets a0through,if x:breaks silently andif x is not None:keeps working. Prefer the one that stays correct under change. - “
x == Noneworks fine.” It usually does, butis Noneis correct:Noneis a singleton (identity is exact), and==can be overridden by a malicious or buggy__eq__, whileiscannot be intercepted. Same reasoning as Integer and String Identity Surprises. - “
bool(x)always succeeds.” False —__bool__can raise (NumPy arrays,NotImplemented), soif x:can raise. Don’t assume a truth test is side-effect-free. - “Empty
dict/set/strare different falsy values, so I can tell them apart inif.” 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
- None True False and the Singletons — the singletons and the
__bool__/__len__mechanism in full (R2). - Dunder Methods and the Data Model — how
__bool__/__len__map to type slots and are dispatched (R6). - Integer and String Identity Surprises — the sibling
is-vs-==gotcha; same “use the most specific test” lesson. - Mutable Default Arguments — the other half of the
def f(x=None)idiom. - The Specializing Adaptive Interpreter — how
TO_BOOLspecializes toTO_BOOL_BOOL/TO_BOOL_INTetc. - Python Internals MOC — §16 Common Gotchas.