PYTHON / CONTROL FLOW
if statements and truthiness
Write if statements that branch on any Python object, and predict which values count as false without converting them yourself.
What you will learn
- Name the built-in falsy values: 0, 0.0, '', [], {}, set(), None, False
- Use `if x:` for emptiness and `if x is None:` for absence, deliberately
- Know that `if obj:` calls __bool__, or __len__ when __bool__ is missing
- Read `not x` as 'bool(x) is False', not as 'x equals False'
Understanding if statements and truthiness
An `if` statement does not require a boolean. Python takes whatever expression follows `if`, asks that object for its truth value, and runs the indented block when the answer is true. The condition `if items:` is therefore shorthand for `if bool(items):`, and the block after it belongs to the `if` purely because of indentation, not because of any brace or keyword.
The truth value comes from the object itself, in a fixed order. Python first looks for a `__bool__` method and uses what it returns; if there is none, it looks for `__len__` and treats length 0 as false and any other length as true; if neither exists, the object is true. That last rule is why an ordinary object, a function, or a module is always truthy, and why the falsy set is small and learnable: `False`, `None`, zero of every numeric type, and every empty built-in container.
The mental model matters most when a value can legitimately be zero or empty. `if count:` is false for both `0` and `None`, so it collapses two different situations into one branch. When you mean "the caller gave me nothing", compare identity with `is None`; when you mean "there is nothing to iterate over", the truthiness test is exactly right. Choosing between them is a statement about your data, not a style preference.
def describe(value):
if value:
print(f"{value!r} is truthy")
if not value:
print(f"{value!r} is falsy")
for v in [0, 1, -1, 0.0, "", " ", [], [0], {}, None]:
describe(v)An if statement converts its condition to a boolean using the object's own __bool__ or __len__, so emptiness and absence are not the same test.
Worked examples
Zero is not the same as missing
Shows how `if not discount:` silently overwrites a real zero that `if discount is None:` preserves.
def apply_discount(price, discount=None):
if discount is None:
discount = 0.10
return round(price * (1 - discount), 2)
def buggy_discount(price, discount=None):
if not discount:
discount = 0.10
return round(price * (1 - discount), 2)
print(apply_discount(100))
print(apply_discount(100, 0.0))
print(buggy_discount(100))
print(buggy_discount(100, 0.0))Example explained
Line 1`if discount is None:` asks only whether the argument was omitted, so a caller-supplied 0.0 survives.
Line 2`if not discount:` is true for None and for 0.0 alike, because both are falsy.
Line 3The second call returns 100.0: an explicit zero discount means full price.
Line 4The fourth call returns 90.0 instead, applying a discount the caller explicitly refused.
Watching Python ask for a truth value
Demonstrates that `if obj:` falls back to __len__ when no __bool__ is defined, and that it is called every time.
class Basket:
def __init__(self, items):
self.items = items
def __len__(self):
print(f" __len__ called on {self.items}")
return len(self.items)
empty = Basket([])
full = Basket(["apple"])
if full:
print("full basket is truthy")
if not empty:
print("empty basket is falsy")
print(bool(full), bool(empty))Example explained
Line 1Basket defines no `__bool__`, so the `if` reaches for `__len__` instead.
Line 2The printed trace appears before each branch message, proving the method runs as part of evaluating the condition.
Line 3A length of 1 makes the object true; a length of 0 makes it false.
Line 4The final line shows `bool()` doing exactly what `if` did, calling `__len__` two more times.
Important notes
A non-empty string is always truthy, including `'0'`, `'False'`, and `' '`; text read from input or a file needs parsing or comparison, not a truthiness test.
`__bool__` must return an actual bool; returning something else raises `TypeError: __bool__ should return bool` when the object is used in a condition.
Common mistakes
Writing `if x == True:` instead of `if x:`, which rejects truthy values like `[1]` or `'hi'` because they are not equal to True, so the block never runs.
Using `if value:` to check that an argument was supplied, then losing every intentional `0`, `0.0`, `''`, or `[]` to the default branch.
Forgetting the colon or the indentation, which raises `SyntaxError: expected ':'` or `IndentationError: expected an indented block` rather than a wrong result.
Try it yourself
Change, predict, then run
Write a function `report(data)` that prints "no data" when `data` is None, "empty" when it is an empty container, and its length otherwise, then call it with None, [], [0], and 0.0 and explain the last result.
Open the Python workspaceCheck your understanding
A class defines `__len__` returning 0 and `__bool__` returning True. What does `if obj:` do?
- Runs the block, because __bool__ is consulted first and __len__ is ignored
- Skips the block, because a length of 0 always means falsy
- Raises TypeError, because the two methods disagree
- Runs the block only if the object is also non-None
Show answer
Python looks for `__bool__` first and stops there when it exists, so the object is true despite its length. Option 2 is tempting because `__len__` governs truthiness for lists and dicts, but that fallback applies only when `__bool__` is absent.