skip to content

Which built-in Python values are falsy inside an `if` statement?

level: juniorimportance: must knowfreq 85%

answer

  1. Two categories, then everything else
  2. Zero, empty, nothing
  3. None and False, plus every empty container
  4. 0, 0.0, 0j, '', [], (), {}, set()
  5. Length zero is falsy; contents never matter

basics

~20 s

Falsy built-ins are None, False, zero of any numeric type (0, 0.0, 0j) and every empty container: '', b'', [], (), {}, set(), range(0). Everything else, including the string '0' and the list [0], is truthy.

solid answer

~40 s

Python asks each value for a single truth bit, and the default answer is truthy. The falsy built-ins fall into three groups: the constants `None` and `False`; zero of any numeric type — `0`, `0.0`, `-0.0`, `0j`, `decimal.Decimal("0")`; and every empty container or sequence — `""`, `b""`, `[]`, `()`, `{}`, `set()`, `range(0)`. Everything else is truthy, which catches people out with `"0"`, `"False"` and `" "` (all non-empty strings) and with `[0]` (a one-element list). Container truthiness is about length, never contents. `not x` returns the opposite as a real `bool`, so `not []` is `True`. Because `if` converts for you, `if x == True:` and `if len(x) > 0:` are both worse spellings of `if x:`.

code

python · 7 lines
python
falsy = [None, False, 0, 0.0, 0j, "", b"", [], (), {}, set(), range(0)]
assert not any(falsy)

# All truthy: non-empty strings and non-empty containers
assert all(["0", "False", [0], {"k": None}, (0,), " "])

print(not [], type(not []))  # True <class 'bool'>

go deeper

for a junior

Be ready to list the falsy built-ins from memory in three groups: None and False, numeric zeros, and every empty container. Then say the default out loud — everything else is truthy.

for a middle

Explain why "0" and [0] are truthy: container truthiness is length-based and string truthiness is emptiness-based, never a judgement about contents. Show that not x returns a real bool while and/or return operands.

for a senior

Demonstrate that you treat if x: as a domain question, not a style one. Point at fields where zero or empty string is legal data and show that you reach for is not None there, especially when parsing config or request payloads.

for a principal

Own the convention: decide whether your APIs distinguish absent from empty at all, and make that explicit in schemas and types rather than leaving each caller to guess which check to write. Inconsistency here is what produces the whole bug class.

## Truthiness is a two-question rule Every `if`, `while`, `not`, `and`, `or`, filter predicate and `assert` in Python asks the same question of a value: *is this object truthy?* Python never requires the object to be `True`. It asks the object to summarise itself as one bit, and unless the object says otherwise, the answer is "yes, I am truthy". That default matters. **An arbitrary object is truthy**; falsiness is the exception a small, memorisable set of built-ins opts into. `bool(x)` is the explicit spelling of the same question, and `if x:` is exactly `if bool(x):` with the conversion done inline by the interpreter. ## The complete built-in falsy set Learn it as three groups rather than as a list of eleven items: 1. **The two constants** — `None` and `False`. 2. **Zero of every numeric type** — `0`, `0.0`, `-0.0`, `0j`, and `decimal.Decimal("0")` or `fractions.Fraction(0, 5)` for the stdlib numeric types. 3. **Every empty container or sequence** — `""`, `b""`, `[]`, `()`, `{}`, `set()`, `frozenset()`, `range(0)`, and an empty `bytearray`, `dict` view or `collections.deque`. ```python falsy = [None, False, 0, 0.0, 0j, "", b"", [], (), {}, set(), range(0)] assert not any(falsy) ``` Anything not in those groups is truthy, and that includes several values people expect to be falsy: the strings `"0"`, `"False"` and `" "` are all non-empty, so all three are truthy. The list `[0]` and the dict `{"k": None}` are truthy too — they are *containers with one element*, and the falsiness of what they contain is irrelevant. Container truthiness is about length, never about contents. ## Zero is not None, and empty is not missing The single most valuable consequence is that **truthiness collapses distinctions your program may care about.** `if x:` treats `0`, `0.0`, `""`, `[]`, `{}` and `None` identically. When a field's domain includes a legitimate zero or a legitimate empty string — a count, a price, a retry budget, a user-supplied comment, a percentile threshold — `if x:` is asking the wrong question. The precise question is `if x is not None:`, which asks only whether the value is absent and lets `0` and `""` through as real data. The mirror image is just as common: reading configuration or environment values as text. An environment variable set to the string `"0"` or `"false"` is a non-empty `str` and therefore truthy, so `if os.environ.get("FEATURE"):` turns the feature *on* for someone who explicitly set it to `0`. The fix is to compare or parse the text deliberately rather than to lean on truthiness. ## `not` always hands back a real `bool` `not x` evaluates the truthiness of `x` and returns the opposite as an actual `bool` object — `True` or `False`, never the operand. That makes `not` different from `and` and `or`, which return one of their operands unchanged. So `not []` is `True` (type `bool`), while `[] or "z"` is the string `"z"`. `not not x` is therefore a working, if unidiomatic, coercion to `bool`; the readable spelling is `bool(x)`, and `operator.truth(x)` is the same conversion as a function you can pass around. Because `if` performs the conversion itself, writing `if bool(x):`, `if x == True:` or `if len(x) > 0:` adds no correctness and loses idiom. `if x == True:` is actively worse: it tests equality against the `True` object, so a truthy value such as `2` or `"yes"` fails the test even though `if x:` would have accepted it. ## Where the values come from Built-in falsiness is not hard-coded per type in the `if` statement. The interpreter asks the object's own type: a type may define `__bool__` to answer directly, or `__len__`, in which case length zero means falsy. That is why every empty container in the standard library is falsy without any of them being special-cased — they all report their length. (Designing those methods for your own classes is a separate subject; what matters here is that the built-in behaviour you rely on is uniform and predictable.) ## One historical wrinkle worth knowing `datetime.time(0, 0)` — midnight — was falsy in very old Python because it was treated as a "zero" time. That was fixed in **Python 3.5**: all `datetime.time` instances are truthy, and on **3.14** `bool(datetime.time(0, 0))` is `True`. It is a good illustration that "zero-like means falsy" is a convention each type chooses, not a law, and that guessing at a type's truthiness instead of checking it is how subtle bugs get written. The rest of the rules above are stable: the built-in falsy set has not changed through **3.14**, and nothing in the deferred-annotation, free-threading or interpreter work of recent releases touches it.

  • Is the string `"0"` truthy, and why does that bite when reading environment variables?
    It is truthy — `"0"` is a non-empty `str`, and only the empty string is falsy. So `if os.environ.get("FEATURE"):` enables a feature for someone who explicitly set it to `0` or `false`, because both are non-empty text. Environment values are always strings, so parse or compare them deliberately rather than testing their truthiness.
  • What does `not x` return, and how is that different from what `x or y` returns?
    `not x` evaluates the truthiness of `x` and returns a real `bool` — `True` or `False`, never the operand. `x or y` returns one of the operands unchanged: `x` if it is truthy, otherwise `y`. So `not []` is `True`, while `[] or "z"` is the string `"z"`. `bool(x)` is the readable way to force a real boolean.
  • Why is `if x:` the wrong test for a field whose value may legitimately be `0`?
    Truthiness collapses `0`, `0.0`, `""`, `[]`, `{}` and `None` into one answer, so `if x:` cannot distinguish "absent" from "legitimately zero or empty". When zero is meaningful — a count, a price, a retry budget — the precise test is `if x is not None:`, which asks only about absence and lets real zeros through.

Truthiness is a bouncer asking "is there anything here?", not "is this the value True?" — an empty box and a box holding a zero get very different answers.

saying these in an interview costs you the question

  • Claims only None and False are falsy
  • Thinks the string '0' or 'False' is falsy
  • Says [0] is falsy because it contains a zero
  • Writes `if x == True:` instead of `if x:`
  • Assumes `if x:` and `if x is not None:` are equivalent
  • Believes `not x` returns the operand rather than a bool

context