Python if Statement Syntax: A Complete Tutorial

The Python if statement is the language's primary tool for making decisions in code. Its syntax is intentionally minimal: the keyword if, followed by a condition, followed by a colon, followed by an indented block. That block runs when the condition is truthy — and does not run when it is falsy. Here is every variation you will ever need, right at the top.

# The three forms you will use every day

score = 74

# 1. Simple if
if score >= 60:
    print("Pass")

# 2. if / else
if score >= 60:
    print("Pass")
else:
    print("Fail")

# 3. if / elif / else
if score >= 90:
    print("A")
elif score >= 80:
    print("B")
elif score >= 70:
    print("C")
elif score >= 60:
    print("D")
else:
    print("F")

# Output: C

Run those three blocks right now and the answer to the question "how does a Python if statement work" will be immediately obvious from the output. The rest of this tutorial goes deeper: how the condition is evaluated, what counts as truthy, how to chain conditions, how to write single-line conditionals, how the walrus operator (:=) lets you assign inside an if condition, and when the match/case statement introduced in Python 3.10 should replace a long elif chain.

The Basic if / elif / else Structure

Python's official language reference defines the if statement as a compound statement — meaning it contains other statements inside it. The grammar, as published in the Python 3 language reference, is:

if_stmt ::= "if" assignment_expression ":" suite
             ("elif" assignment_expression ":" suite)*
             ["else" ":" suite]

In plain terms: one if (required), zero or more elif branches (optional), and one else (optional). Python evaluates each condition in order from top to bottom. The first one that is truthy wins, its indented block executes, and every remaining branch is skipped. If no condition is truthy and an else exists, the else block runs. If no condition is truthy and there is no else, nothing happens — Python silently moves on.

# Demonstrating the "first truthy wins" rule
x = 50

if x > 100:
    print("greater than 100")   # skipped — False
elif x > 75:
    print("greater than 75")    # skipped — False
elif x > 25:
    print("greater than 25")    # executes — True
elif x > 10:
    print("greater than 10")    # skipped — never reached
else:
    print("10 or below")        # skipped — never reached

# Output: greater than 25

Notice that x > 10 is also true, but Python never checks it because an earlier branch already matched. This short-circuit evaluation is intentional and important for performance — it means you should put the most commonly satisfied (or most specific) conditions first.

Note

Python uses indentation, not curly braces, to define the body of each branch. The standard is 4 spaces per level. Mixing tabs and spaces in the same file raises an IndentationError in Python 3. Pick one and stay consistent — nearly every Python style guide, including PEP 8, recommends spaces.

How many elif branches can you have?

There is no hard limit. In practice, if you find yourself writing more than three or four elif branches all testing the same variable for equality (e.g., if status == 200 ... elif status == 404 ...), the match/case statement covered in section six is almost always the cleaner choice. For conditions that test different variables or use inequality comparisons (<, >, in, is), a chain of elif remains the right tool.

# Mixed-condition elif chains are a good fit for if/elif
temperature = 38.5
is_raining = True
wind_speed = 12

if temperature > 40:
    advice = "extreme heat warning — stay indoors"
elif temperature > 35 and is_raining:
    advice = "hot and humid — carry an umbrella"
elif temperature > 35:
    advice = "hot — stay hydrated"
elif wind_speed > 30:
    advice = "strong wind warning"
else:
    advice = "conditions normal"

print(advice)
# Output: hot and humid — carry an umbrella

Truthiness, Comparison Operators, and Boolean Logic

Python evaluates the condition in an if statement by calling bool() on it internally. The result is either True or False. Understanding what evaluates to False is the more useful skill, because the list of "falsy" values is short and fixed:

Value Type Why it is falsy
False bool The boolean false literal
None NoneType Represents the absence of a value
0, 0.0, 0j int, float, complex Numeric zero in any type
"" str Empty string
[], (), {}, set() list, tuple, dict, set Empty container
b"" bytes Empty bytes object

Everything else is truthy. This means you can test whether a list has items, whether a string is non-empty, or whether a variable has been set — all without an explicit comparison operator:

# Idiomatic Python: rely on truthiness rather than explicit == comparisons

items = [1, 2, 3]
if items:                        # True because the list is non-empty
    print(f"processing {len(items)} items")

username = ""
if not username:                 # True because empty string is falsy
    print("username is required")

result = None
if result is None:               # Explicit None check — preferred over "if not result"
    print("no result yet")

count = 0
if not count:                    # True because 0 is falsy
    print("nothing to count")
Pro Tip

Use is None and is not None — not == None — when checking for None. The is operator tests identity (same object in memory), not equality. Since there is only ever one None object in a Python process, is None is both more correct and marginally faster than == None. PEP 8 explicitly recommends this pattern.

Combining conditions with and, or, not

Python uses the English keywords and, or, and not for boolean logic — not &&, ||, and ! as in many other languages. Both and and or short-circuit: and stops evaluating as soon as it finds a falsy operand; or stops as soon as it finds a truthy one.

age = 22
has_ticket = True
is_vip = False

# and — both sides must be truthy
if age >= 18 and has_ticket:
    print("entry permitted")

# or — at least one side must be truthy
if is_vip or age >= 21:
    print("access to VIP lounge")

# not — inverts the truthiness
if not is_vip:
    print("standard access only")

# Combining all three — use parentheses for clarity
if (age >= 18 and has_ticket) or is_vip:
    print("welcome")

Comparison operators quick reference

x = 10

x == 10      # equal to              -> True
x != 5       # not equal to          -> True
x > 5        # greater than          -> True
x < 20       # less than             -> True
x >= 10      # greater than or equal -> True
x <= 10      # less than or equal    -> True

# Python allows chained comparisons — a unique and readable feature
1 < x < 100          # equivalent to: 1 < x and x < 100  -> True
0 <= x <= 10         # True
5 < x < 15           # True

# in — membership test (works on strings, lists, tuples, sets, dicts)
"a" in "cat"         # True
3 in [1, 2, 3, 4]    # True
"key" in {"key": 1}  # True (tests dict keys, not values)

# is — identity test
y = None
y is None            # True
y is not None        # False
"Readability counts." — Tim Peters, PEP 20 — The Zen of Python

Chained comparisons like 1 < x < 100 are one of the places where Python's syntax is genuinely more readable than most other languages. Each middle operand (x in this case) is evaluated only once, which also avoids redundant computation when the middle value is an expensive function call.

Nested if Statements

Any statement inside an if block can itself be another if statement. Nesting is the right tool when a second check only makes sense after a first check has already passed. The classic mistake is going too many levels deep — three or more levels of nesting is usually a signal to refactor.

# Nested if — appropriate because the inner check
# depends on the outer check passing first
def process_payment(user, amount):
    if user.is_authenticated:
        if user.balance >= amount:
            if amount > 0:
                user.balance -= amount
                return f"Payment of ${amount} processed"
            else:
                return "Amount must be greater than zero"
        else:
            return "Insufficient funds"
    else:
        return "User not authenticated"

Three levels deep is already pushing the limit. A common refactoring technique called "early return" flattens the nesting by returning immediately when a condition fails, rather than wrapping the happy path in more branches:

# Same logic, refactored with early returns — much easier to read
def process_payment(user, amount):
    if not user.is_authenticated:
        return "User not authenticated"
    if amount <= 0:
        return "Amount must be greater than zero"
    if user.balance < amount:
        return "Insufficient funds"

    user.balance -= amount
    return f"Payment of ${amount} processed"

The early-return version has zero nesting and identical behavior. Each guard condition is checked at the top, failures exit immediately, and the successful case is reached only when all guards have passed. This pattern is widely favored in production Python code precisely because it reduces cognitive load.

The Ternary (Conditional) Expression

Python has a one-line conditional expression — often called a ternary operator — with this syntax:

value_if_true if condition else value_if_false

Notice the order: the "true" value comes first, then the condition, then the "false" value. This is the opposite of most other languages' condition ? true_value : false_value syntax, and it takes a moment to internalize. The upside is that it reads almost like English: "give me X if this is true, otherwise give me Y."

# Ternary expression examples

age = 20
status = "adult" if age >= 18 else "minor"
print(status)   # adult

# Assigning a default value
config_value = None
timeout = config_value if config_value is not None else 30
print(timeout)  # 30

# Inside f-strings
score = 83
print(f"Result: {'pass' if score >= 60 else 'fail'}")  # Result: pass

# In a function return
def abs_val(n):
    return n if n >= 0 else -n

print(abs_val(-7))   # 7
print(abs_val(4))    # 4
Watch Out

Avoid chaining ternary expressions. a if x else b if y else c is technically valid Python but nearly impossible to read at a glance. If your ternary needs more than one else, write it as a full if/elif/else block instead.

The Walrus Operator Inside if Conditions

Python 3.8 introduced the assignment expression operator :=, which assigns a value to a variable and returns that value as part of a single expression. Its colloquial name — the walrus operator — comes from the fact that := resembles a walrus lying on its side.

The PEP 572 specification describes its core purpose as enabling you to assign variables in places where assignment statements are ordinarily prohibited, such as inside the condition of an if statement or a while loop.

# Without walrus — two lines to assign and then test
import re

data = "order:12345"
match = re.search(r"\d+", data)
if match:
    print(f"Order number: {match.group()}")

# With walrus — assign and test in one expression
if match := re.search(r"\d+", data):
    print(f"Order number: {match.group()}")

# Output (both versions): Order number: 12345

The walrus operator is especially useful when you want to avoid calling the same function twice — once to check whether the result is valid and again to use it:

# Anti-pattern: calling an expensive function twice
import json

raw = '{"user": "alice", "score": 95}'

def parse_response(text):
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return None

# Without walrus — parse_response called twice
if parse_response(raw):
    data = parse_response(raw)
    print(f"Score: {data['score']}")

# With walrus — parsed once, result available inside the block
if data := parse_response(raw):
    print(f"Score: {data['score']}")

# Output: Score: 95
"Assignment expressions allow a value to be assigned to a variable as part of an expression — particularly in loops and conditional expressions." — PEP 572, Python Enhancement Proposals

The walrus operator inside a while loop is another high-value use case. Reading input or polling a data source line by line previously required a setup assignment before the loop and a second assignment at the bottom of the loop body. The walrus operator collapses that into a single expression in the condition:

# Classic pattern before Python 3.8 — redundant assignment
line = input("Enter text (blank to quit): ")
while line:
    print(f"You entered: {line}")
    line = input("Enter text (blank to quit): ")

# Modern walrus pattern — no duplication
while line := input("Enter text (blank to quit): "):
    print(f"You entered: {line}")
Pro Tip

Always wrap walrus expressions in parentheses when they appear inside a larger condition. Writing if (n := len(data)) > 100: is clearer than if n := len(data) > 100: — the latter assigns the boolean result of the comparison to n, which is almost certainly not what you want. The parentheses force the assignment to happen first, then the comparison is applied to the assigned value.

When to Use match / case Instead of elif

Python 3.10 introduced structural pattern matching via the match/case statement (specified in PEP 634). It is not a replacement for if — it is a complement. The clearest signal to reach for match/case is when you are testing one variable against several specific values and the branches do not involve inequality comparisons.

# If / elif chain — works, but verbose when testing one value for equality
def http_status(code):
    if code == 200:
        return "OK"
    elif code == 201:
        return "Created"
    elif code == 400:
        return "Bad Request"
    elif code == 401:
        return "Unauthorized"
    elif code == 403:
        return "Forbidden"
    elif code == 404:
        return "Not Found"
    elif code == 500:
        return "Internal Server Error"
    else:
        return "Unknown status"

# match / case — cleaner for this pattern (requires Python 3.10+)
def http_status(code):
    match code:
        case 200:
            return "OK"
        case 201:
            return "Created"
        case 400:
            return "Bad Request"
        case 401:
            return "Unauthorized"
        case 403:
            return "Forbidden"
        case 404:
            return "Not Found"
        case 500:
            return "Internal Server Error"
        case _:
            return "Unknown status"

print(http_status(404))   # Not Found
print(http_status(201))   # Created

The _ wildcard in case _: is the catch-all equivalent of else. It matches anything not caught by a previous case. Unlike else, it is entirely optional — if no case matches and there is no case _:, Python moves on silently, exactly as it does with an if block that has no else.

The structural part of pattern matching

What makes match/case genuinely powerful — beyond a simple switch — is that it can match on the shape of data and unpack it into variables in the same step. The PEP 636 tutorial describes this as doing two things at once: verifying that the subject has a certain structure, and binding names to component elements of that structure.

# Matching on data structure — not just values
def describe_point(point):
    match point:
        case (0, 0):
            return "origin"
        case (x, 0):
            return f"on x-axis at {x}"
        case (0, y):
            return f"on y-axis at {y}"
        case (x, y):
            return f"point at ({x}, {y})"
        case _:
            return "not a point"

print(describe_point((0, 0)))    # origin
print(describe_point((5, 0)))    # on x-axis at 5
print(describe_point((3, 7)))    # point at (3, 7)

# Matching with a guard — adding an if condition to a case
def classify_number(n):
    match n:
        case x if x < 0:
            return "negative"
        case 0:
            return "zero"
        case x if x % 2 == 0:
            return "positive even"
        case _:
            return "positive odd"

print(classify_number(-3))   # negative
print(classify_number(0))    # zero
print(classify_number(8))    # positive even
print(classify_number(7))    # positive odd

The guard in case x if x < 0: is exactly the if statement you already know — used here as an additional filter after the structural match has succeeded. As the Real Python guide to structural pattern matching notes, patterns are static (they describe structure), while guards express arbitrary logical conditions checked dynamically at runtime.

Scenario Better choice Reason
One variable, tested for equality against several constants match/case Cleaner, no repeated variable name
Multiple variables, inequality operators (<, >) if/elif match/case patterns are structural, not relational
Unpacking and matching data shapes (tuples, dicts, classes) match/case Simultaneous structure check and variable binding
Simple two-branch decision if/else match/case is heavier syntax for a simple case
Inline assignment needed in condition if + walrus := match/case does not support inline assignment in the subject

Common Mistakes and How to Avoid Them

No tutorial on Python if statement syntax is complete without the mistakes that show up in real code. These are not edge cases — they are things that trip up programmers at every level.

Using = instead of == in a condition

# WRONG — this is a SyntaxError in Python (intentionally prevented)
# if x = 10:     <-- Python will not allow this
#     print("ten")

# CORRECT — use == for comparison
x = 10
if x == 10:
    print("ten")

Python raises a SyntaxError if you try to use = inside an if condition. This was a deliberate design choice to prevent the class of bugs common in C and JavaScript where if (x = 0) silently assigns zero to x and then evaluates as false. The walrus operator := is the explicit, opt-in way to do assignment in a condition when you genuinely need it.

Off-by-one in range checks

score = 60

# WRONG — this misclassifies a score of exactly 60
if score > 60:
    grade = "pass"
else:
    grade = "fail"
# score == 60 falls into "fail" — probably not intended

# CORRECT — use >= if the boundary should be inclusive
if score >= 60:
    grade = "pass"
else:
    grade = "fail"
# score == 60 is now "pass"

Comparing to True or False explicitly

is_active = True

# NOT WRONG — but unnecessarily verbose, and PEP 8 discourages it
if is_active == True:
    print("active")

# CORRECT — rely on truthiness directly
if is_active:
    print("active")

# Also correct — when you need the negative
if not is_active:
    print("inactive")

Forgetting the colon

# WRONG — SyntaxError: expected ':'
# if x > 0
#     print("positive")

# CORRECT — the colon is mandatory
x = 5
if x > 0:
    print("positive")

Python 3.13 improved error messages significantly. As the Python 3.13 release notes describe, the interpreter now highlights the relevant line in color by default and provides more specific guidance. Where earlier versions might produce a generic SyntaxError, Python 3.13 will point directly at the missing colon. If you are learning on an older version, upgrading to 3.13 or later makes debugging syntax errors noticeably less frustrating.

Misaligned indentation in elif or else

x = 5

# WRONG — elif must align with its if, not be indented inside it
# if x > 0:
#     print("positive")
#     elif x == 0:       <-- IndentationError
#         print("zero")

# CORRECT — if, elif, and else are all at the same indentation level
if x > 0:
    print("positive")
elif x == 0:
    print("zero")
else:
    print("negative")

Key Takeaways

  1. Core syntax never changes: if condition: followed by an indented block is the foundation. elif and else are optional extensions. Python evaluates each branch top-to-bottom and executes only the first truthy match.
  2. Truthiness is your friend: Python evaluates conditions by converting them to bool. Empty containers, zero, None, and False are all falsy. Everything else is truthy. Use if items: instead of if len(items) > 0:.
  3. Early returns beat deep nesting: When a function has multiple guard conditions, return early on failure. This eliminates nesting and puts the happy path at the end with no indentation overhead.
  4. The walrus operator (:=) is for assign-then-test patterns: Introduced in Python 3.8, it removes redundant assignments before if conditions. Always wrap it in parentheses when combined with a comparison operator.
  5. Use match/case for structural matching: If you are testing one subject against many specific values or data shapes, match/case (Python 3.10+) is cleaner than a long elif chain. For inequality-based or multi-variable conditions, stick with if/elif.
  6. Python 3.13 error messages are better: The latest Python release highlights syntax errors in color and provides more specific guidance. If you are seeing unhelpful error messages on an older version, this alone is a reason to upgrade.

The if statement is one of those constructs that looks trivial the first time you see it but reveals new depth the more you use it. Chained comparisons, truthiness-based checks, the walrus operator, and structural pattern matching all grow from that same two-line foundation: a keyword, a condition, a colon, and an indented block. Get that foundation solid — by running every code example above and modifying the values — and the rest follows naturally.