if/elif/else and Python truthiness
Overview
Python evaluates an `if`/`elif`/`else` chain top-down and runs the FIRST block whose condition is truthy, then skips all remaining branches. Both `elif` and `else` are optional. status = 404 if status == 200: print('ok') elif status == 404: print('not found') else: print('other') Python's truthiness rules determine what counts as truthy or falsy without an explicit comparison. Falsy values include: - `False` and `None` - Numeric zeros (`0`, `0.0`) - Empty sequences (`""`, `[]`) - Empty collections (`{}`, `set()`) Everything else is truthy. So `if response:` works when `response` is a non-empty list, and `if not user:` works when `user` is `None`. Think of truthiness as Python asking "does this value carry meaningful content?" — zero-quantities and empty containers say no. A common trap is using separate `if` statements when you meant to use `elif`. This causes multiple branches to execute when conditions overlap: status = 404 if status >= 400: print('error') # Executes! if status == 404: print('not found') # Also executes! Another trap is writing multi-line blocks just to assign a single variable based on a condition: if config.get('debug'): level = 'DEBUG' else: level = 'INFO' This verbose pattern obscures intent and is replaced by inline conditional evaluation (covered fully in `ternary-expressions`). Finally, when checking a single variable against many exact values, long `elif` chains become hard to maintain: if status == 400: return 'Bad Request' elif status == 404: return 'Not Found' elif status == 500: return 'Internal Error' This anti-pattern is solved using structural pattern matching (covered fully in `match-case`).
Truthiness lets you write concise guards (`if items:` instead of `if len(items) > 0:`). The danger is when `0` or `""` are valid values that must NOT be treated as "missing" — a naked truthiness check would silently skip them.
Where used: FastAPI route guards (`if not current_user: raise 401`), Pydantic field validators, CLI argument presence checks
Why learn this
- Writing correct guards in FastAPI handlers that distinguish `None` from `0` or an empty string
- Understanding why `if value:` can silently hide a valid `0` — the foundation of safe API input validation
Code walkthrough
items = []
count = 0
name = 'alice'
if items:
print('has items')
if count:
print('count nonzero')
if name:
print('has name')
print('done')
Focus: `items` (empty list) and `count` (zero) are falsy so their blocks are skipped; `name` (`'alice'`) is truthy so only `'has name'` prints before `'done'`.
Common mistakes
- Using truthiness when `0` or `""` are valid inputs: `if count:` returns `False` for `count=0`, silently skipping valid data. Fix: use explicit checks (`if count is None:`) when `0` is a meaningful value.
- Forgetting `elif` stops the chain: Using separate `if` statements instead of `elif` means multiple branches can execute. Use `elif` when conditions are mutually exclusive.
Glossary
- truthiness
- How Python decides if a value is considered true or false in an `if`-statement without you having to explicitly check. Example: `if items:` is `True` when `items` is a non-empty list and `False` when it is empty.
- falsy
- Values that Python automatically treats as false, such as zero, empty lists, or `None`. Example: `if not []:` evaluates to `True` because an empty list is falsy.
Recall questions
- Which values does Python treat as falsy?
- If the first branch of an `if`/`elif` chain matches, what happens to the remaining `elif` and `else` branches?
- When is using `if value:` unsafe?
- What is the difference between chaining `elif` vs using separate `if` statements?
- What is a more maintainable alternative to a long `if`/`elif` chain checking a single variable against many exact values?
Understanding checks
What will this code print?
`error`
Python evaluates an `if`/`elif` chain top-down and executes only the FIRST matching block. Since `404 >= 400` is true, it prints `'error'` and skips the remaining branches, even though `status == 404` is also true.
This code is intended to use the `timeout` from the `config`, but falls back to `10` if missing. It fails when `timeout=0` is passed in (which is a valid setting). Why?
It evaluates truthiness, and `0` is falsy, so it incorrectly falls back to `10`.
Truthiness checks like `not timeout` treat `0` as falsy. When `0` is a valid meaningful value, you must use an explicit check like `if timeout is None:`.
Practice tasks
HTTP status classifier
Modify `classify_status(code)` to return `'success'` for 2xx codes, `'client_error'` for 4xx, `'server_error'` for 5xx, and `'unknown'` for anything else. Use `if`/`elif`/`else`.
Challenge
Config timeout guard
Write `get_timeout(config, default)` that returns `config['timeout']` when it is present, not `None`, and a positive integer. Return `default` in all other cases. Use explicit checks, not truthiness.
Continue learning
Previous: Primitive types: int, float, bool, str
Previous: Dynamic typing and type()
Next: Ternary expressions
Next: match-case statement
Next: for and while loops