The if, elif, and else Statements

Updated

September 7, 2026

The if, elif, and else Statements

After reading this chapter, you will master multi-branch decision trees using if, elif, and else, evaluate truthiness and falsy values across standard types, leverage short-circuiting for defensive execution, and apply the Bouncer pattern to eliminate deeply nested conditional logic.

Mental model

Python evaluates conditional blocks linearly from top to bottom. As soon as a branch condition evaluates to a truthy value, CPython executes that branch’s block and immediately skips all subsequent elif and else blocks:

Branching Execution Pipeline:
           [ if condition ]
               /        \
           True          False
            /               \
    [ if block ]     [ elif condition ]
            │            /        \
            │        True          False
            │         /               \
            │   [ elif block ]    [ else block ]
            │         │               │
            ▼         ▼               ▼
          └───────────┴───────────────┘
                        │
                        ▼
                [ Resume Program ]

Short-Circuit Evaluation Guarding

Logical operators and and or stop evaluating operands as soon as the outcome is mathematically guaranteed:

  • A and B: If A is falsy, Python returns A immediately without ever evaluating B.
  • A or B: If A is truthy, Python returns A immediately without ever evaluating B.
Defensive Guard Pipeline:
  user is not None and user.is_active and check_permissions(user)
         │
         ├─ If user is None ──────▶ Short-circuits! Never queries user.is_active
         └─ If user is not None
                  │
                  └─ If not active ──▶ Short-circuits! Never calls check_permissions()

Minimal example

Save as if_decision_trees.py:

# if_decision_trees.py
def classify_http_status(status_code: int) -> str:
    """Classify HTTP response codes using a clean decision ladder."""
    if 200 <= status_code < 300:
        return "SUCCESS"
    elif 300 <= status_code < 400:
        return "REDIRECTION"
    elif 400 <= status_code < 500:
        return "CLIENT_ERROR"
    elif 500 <= status_code < 600:
        return "SERVER_ERROR"
    else:
        return "INVALID_STATUS"

def main() -> None:
    test_codes = [200, 301, 404, 503, 999]
    for code in test_codes:
        category = classify_http_status(code)
        print(f"HTTP {code:3d} -> Category: {category}")

if __name__ == "__main__":
    main()

Run via uv run python if_decision_trees.py:

HTTP 200 -> Category: SUCCESS
HTTP 301 -> Category: REDIRECTION
HTTP 404 -> Category: CLIENT_ERROR
HTTP 503 -> Category: SERVER_ERROR
HTTP 999 -> Category: INVALID_STATUS

Worked examples

Case 1: The Bouncer Pattern (Guard Clauses vs Arrow Anti-Pattern)

Deeply nested if statements create the “Arrow Anti-Pattern” (code indenting further and further to the right). The Bouncer Pattern inverts checks into early returns, keeping the happy path at indentation level 0:

# bouncer_pattern.py
from typing import Any

# THE ANTI-PATTERN (Arrow Code):
def process_request_nested(request: dict[str, Any]) -> str:
    if request:
        if "user" in request:
            if request["user"].get("is_authenticated"):
                if request["user"].get("has_permission"):
                    return "Action Authorized"
                else:
                    return "Forbidden: Missing Permission"
            else:
                return "Unauthorized: User Not Authenticated"
        else:
            return "Bad Request: No User Field"
    else:
        return "Bad Request: Empty Payload"

# THE IDIOMATIC PATTERN (Bouncer Clauses):
def process_request_clean(request: dict[str, Any]) -> str:
    # Guard 1: Empty request
    if not request:
        return "Bad Request: Empty Payload"
    
    # Guard 2: Missing user
    user = request.get("user")
    if not user:
        return "Bad Request: No User Field"
    
    # Guard 3: Authentication
    if not user.get("is_authenticated"):
        return "Unauthorized: User Not Authenticated"
    
    # Guard 4: Permission
    if not user.get("has_permission"):
        return "Forbidden: Missing Permission"

    # Happy path at top-level indentation
    return "Action Authorized"

if __name__ == "__main__":
    sample_request = {
        "user": {
            "is_authenticated": True,
            "has_permission": True,
        }
    }
    print("Nested result:", process_request_nested(sample_request))
    print("Clean result :", process_request_clean(sample_request))

Run:

uv run python bouncer_pattern.py

Output:

Nested result: Action Authorized
Clean result : Action Authorized

Case 2: Truthiness and The Numeric 0 Trap

Every Python object possesses a boolean truth value. In Python, the following objects evaluate to False: - None and False - Numeric zeros: 0, 0.0, 0j - Empty containers: "", (), [], {}, set()

Everything else evaluates to True. When validating numeric metrics, checking if not metric: incorrectly treats 0 as an error:

# falsy_metric_audit.py
def record_dropped_packets(count: int | None) -> None:
    # DANGEROUS: If count is 0, 'not count' evaluates to True!
    if not count:
        print(f"[Naive Check] Invalid or zero metric: {count}")

    # SAFE: Explicitly check for None
    if count is None:
        print("[Safe Check] Metric was omitted (None).")
    else:
        print(f"[Safe Check] Valid metric recorded: {count} dropped packets.")

if __name__ == "__main__":
    record_dropped_packets(0)     # Zero is a perfectly healthy metric!
    record_dropped_packets(None)  # Truly missing metric

Run:

uv run python falsy_metric_audit.py

Output:

[Naive Check] Invalid or zero metric: 0
[Safe Check] Valid metric recorded: 0 dropped packets.
[Naive Check] Invalid or zero metric: None
[Safe Check] Metric was omitted (None).

Pitfalls

Pitfall 1: Comparing Explicitly to True or False

# ANTI-PATTERN:
if is_ready == True:
    pass

# ANTI-PATTERN:
if is_ready is True:
    pass

# IDIOMATIC PYTHON:
if is_ready:
    pass

Direct comparisons to True break custom classes implementing __bool__() and violate Pythonic conventions.

Pitfall 2: Combining and with or Without Parentheses

Python evaluates not before and, and and before or. Ambiguous unparenthesized chains cause security bypasses:

# AMBIGUOUS:
# is_admin or is_manager and is_verified
# Python parses this as: is_admin or (is_manager and is_verified)

# EXPLICIT AND SAFE: Always group multi-operator logic
is_allowed = (is_admin or is_manager) and is_verified

Exercises

  1. Write an if/elif/else function classify_temperature(temp_c: float) -> str that categorizes temperatures into "FREEZING", "COLD", "MODERATE", and "HOT" using chained comparisons (0.0 <= temp_c < 15.0).
  2. Refactor a three-level nested dictionary check into guard clauses using the Bouncer pattern.
  3. Demonstrate why bool([]) is False while bool([0]) is True.
  4. Write a conditional check that safely inspects sys.argv[1] only if len(sys.argv) > 1 using short-circuit evaluation.

Further reading

  • Python Language Reference: The if statement.
  • Python Standard Library: Truth Value Testing.
  • PEP 8: Style Guide for Python Code — Programming Recommendations for if statements.