Tracing conditionals

Big Idea

A conditional is a decision point. Python evaluates a condition, gets either True or False, and takes exactly one path. The lines in every other path are skipped entirely — they do not run, they do not change variables, they do not exist for this execution. Tracing conditionals means following the path that actually runs, ignoring the paths that do not, and being precise about why.

This article is part of the Code Reading, Tracing & Prediction section. It assumes you are comfortable with basic trace tables. If trace tables are new to you, read Trace Tables first. The techniques here build directly on that foundation.

LevelWhat it coversWhen to read it
Basic ideaHow to trace a simple if/else; how to record the decision in a trace tableRight now.
At a deeper levelelif chains, nested conditionals, and compound conditions with and/or/notOnce you have traced a simple if/else on your own.
At the deepest levelHow Python evaluates conditions; short-circuit evaluation; truthinessWhen the basic and deeper levels feel solid and you want to understand what Python is actually doing.

1   The Core Rule

Basic Idea

Every if statement makes a binary decision: the condition is either True or False. Exactly one branch runs. All other branches are completely skipped.

In a trace table, a conditional adds two things:

  1. A decision row — you write the condition, substitute the current variable values, evaluate it, and state which branch you are taking.
  2. Rows only for the lines inside the branch that runs. Lines inside branches that do not run do not appear in the table at all.

This is the most common mistake when tracing conditionals: including rows for code that was skipped. If a branch did not run, it leaves no trace. Do not give it rows.

At a Deeper Level

Here is the simplest possible conditional program:

temperature = 35
if temperature > 30:
    message = "hot"
else:
    message = "fine"
print(message)

Before tracing, identify the variable values at the moment the condition is reached. temperature is 35. Evaluate 35 > 30: that is True. Take the if branch. Skip the else branch entirely.

LinetemperaturemessageOutput
temperature = 3535  
if temperature > 30:35 > 30 → True. Take the if branch. Skip else.
    message = "hot" "hot" 
print(message)  hot

The line message = "fine" inside the else block does not appear in the table. It did not run. It does not exist for this execution.

Now trace the same program with temperature = 20. The condition 20 > 30 is False. This time the else branch runs and the if branch is skipped.

LinetemperaturemessageOutput
temperature = 2020  
if temperature > 30:20 > 30 → False. Skip if branch. Take else.
    message = "fine" "fine" 
print(message)  fine

Same code, different input, different path. This is the central feature of conditionals: the execution path depends on the data. Tracing the same program with multiple inputs is one of the best ways to understand what it actually does.

Always write out the evaluation in the decision row

Do not write just "True" in the decision row. Write the full evaluation: substitute the current variable values, show the comparison, state the result, and name the branch you are taking. This forces you to use the actual current values rather than what you assume them to be. Most conditional tracing errors happen because the programmer used an assumed or outdated value at the decision point.

At the Deepest Level

When Python evaluates a condition like temperature > 30, it constructs a temporary Boolean value — True or False — that exists in memory for the duration of the branch decision and is then discarded. This Boolean is not stored in any named variable (unless you explicitly store it with something like result = temperature > 30).

Python's conditional execution is implemented through jump instructions at the bytecode level. When a condition evaluates to False, Python executes a jump instruction that moves the instruction pointer to the first line after the skipped block. The skipped lines are not evaluated, not partially executed, and not considered. They are entirely bypassed. This is why it is accurate to say those lines do not exist for that execution — at the machine level, they genuinely are not visited.

2   Tracing elif Chains

Basic Idea

An elif chain tests conditions one at a time, in order, from top to bottom. The moment one condition is True, that branch runs and the rest of the chain is skipped — even if a later condition would also be True. If no condition is True and there is an else, the else branch runs.

To trace an elif chain:

  1. Evaluate the first condition. If it is True, take that branch and skip everything else in the chain.
  2. If it is False, move to the first elif and evaluate its condition.
  3. Continue until a condition is True or the chain is exhausted.
  4. If an else exists and nothing was True, take the else branch.

Only one branch ever runs, no matter how many conditions you have.

At a Deeper Level

Here is a program with an elif chain to trace with score = 73:

score = 73
if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
elif score >= 60:
    grade = "D"
else:
    grade = "F"
print(grade)
LinescoregradeOutput
score = 7373  
if score >= 90:73 >= 90 → False. Move to elif.
elif score >= 80:73 >= 80 → False. Move to next elif.
elif score >= 70:73 >= 70 → True. Take this branch. Skip remaining elif and else.
    grade = "C" "C" 
print(grade)  C

Once score >= 70 evaluated to True, the remaining elif score >= 60 and the else were skipped entirely. They do not appear in the table.

Each False gets its own decision row

A common shortcut is to write one row that says "all other conditions were False." Avoid this. Write a separate decision row for every condition that was actually evaluated. When a bug is caused by conditions being tested in the wrong order, the individual rows make it visible. Collapsing them hides it.

Now trace the same program with score = 55. Every condition will be False and the else branch will run. Write out every decision row.

LinescoregradeOutput
score = 5555  
if score >= 90:55 >= 90 → False.
elif score >= 80:55 >= 80 → False.
elif score >= 70:55 >= 70 → False.
elif score >= 60:55 >= 60 → False. No conditions matched. Take else.
    grade = "F" "F" 
print(grade)  F
At the Deepest Level

The ordering of conditions in an elif chain matters — and the grading example above demonstrates why. Because the first matching condition wins and the rest are skipped, the conditions must be arranged so that more specific cases come before more general ones. In the grading example, if the conditions were reversed — testing score >= 60 first — then any score of 60 or above would immediately match, and the B, A branches would never be reached.

This ordering dependency is a common source of bugs. A trace table that evaluates each condition explicitly, with the actual values substituted in, will catch an ordering bug immediately — the wrong branch will be taken and the trace will reveal it. A programmer who tries to reason about the chain without tracing it may not notice until they test with a boundary value.

3   Conditionals Without an else

Basic Idea

Not every conditional has an else. When a condition is False and there is no else, nothing happens — execution continues with the line after the entire if block. The variable affected by the if keeps whatever value it had before.

score = 45
bonus = 0
if score > 90:
    bonus = 10
print(score + bonus)

If score is 45, the condition is False. There is no else. bonus stays at 0. The program prints 45.

At a Deeper Level
LinescorebonusOutput
score = 4545  
bonus = 0 0 
if score > 90:45 > 90 → False. No else. Skip if block. Continue.
print(score + bonus)  45

The decision row explicitly notes "No else. Continue." This is worth writing because it forces you to confirm that you are not imagining an else that is not there. Beginners sometimes assume a variable must have been set somewhere, and trace as if an else existed.

Variables hold their previous value when a branch is skipped

When a conditional block is skipped, every variable inside it keeps the value it had before the condition was evaluated. Nothing is reset. Nothing changes. The skipped block is invisible to that execution. If a variable was never assigned before the conditional, and the conditional is skipped, the variable simply does not exist yet — and any attempt to use it after will cause a NameError.

At the Deepest Level

A conditional without an else is sometimes called a guard: it only takes action when a specific condition is met and does nothing otherwise. Guards are very common in professional code for input validation, early exits, and optional behaviour.

The risk of a guard is an uninitialised variable. If a variable is only assigned inside an if block with no else, and the condition is False, the variable will not exist after the block. Python will raise a NameError the moment anything tries to use it. The fix is to always assign a default value before the conditional — exactly as bonus = 0 is assigned before the if in the example above. This pattern is so reliable and readable that it is considered good practice regardless of whether you expect the condition to be False.

4   Compound Conditions: and, or, not

Basic Idea

A compound condition combines two or more comparisons into a single condition using the keywords and, or, and not.

KeywordWhat it meansResult is True when...
andBoth parts must be TrueThe left side is True and the right side is also True
orAt least one part must be TrueThe left side is True or the right side is True (or both)
notReverses the resultThe condition after not is False

When tracing a compound condition, evaluate each part separately before combining them. Do not try to hold the whole thing in your head at once.

At a Deeper Level

Here is a program using a compound condition. Trace it with age = 17 and has_ticket = True:

age = 17
has_ticket = True
if age >= 18 and has_ticket:
    print("Entry granted")
else:
    print("Entry denied")

In the decision row, evaluate each part in order:

Lineagehas_ticketOutput
age = 1717  
has_ticket = True True 
if age >= 18 and has_ticket:age >= 18 → 17 >= 18 → False. has_ticket → True. False and True → False. Take else.
    print("Entry denied")  Entry denied

Even though has_ticket is True, the overall condition is False because and requires both parts to be True. One False part is enough to make the whole and expression False.

Now trace with age = 19 and has_ticket = False:

Lineagehas_ticketOutput
age = 1919  
has_ticket = False False 
if age >= 18 and has_ticket:age >= 18 → 19 >= 18 → True. has_ticket → False. True and False → False. Take else.
    print("Entry denied")  Entry denied

Entry is still denied. Old enough, but no ticket. Both conditions must be True for and to succeed. To get "Entry granted", you need age = 19 and has_ticket = True.

Evaluate each part separately, left to right

When writing the decision row for a compound condition, always evaluate left to right, one part at a time, showing the intermediate results. Write: age >= 18 → True. has_ticket → False. True and False → False. Never write just "False" without showing the working. The working is where the understanding lives.

At the Deepest Level

Python uses short-circuit evaluation for and and or. This means Python stops evaluating as soon as the result is certain — it does not always evaluate both sides.

For and: if the left side is False, the result must be False regardless of the right side. Python does not evaluate the right side at all.

For or: if the left side is True, the result must be True regardless of the right side. Python does not evaluate the right side at all.

This matters for tracing when the right side has a side effect — a function call, for instance. If the left side short-circuits, the right side's function never runs. In Grade 9 work this is rare, but it explains behaviour that can otherwise seem mysterious. When you write out the evaluation left to right in your decision row, note where evaluation stopped if it short-circuited.

not does not short-circuit — it always evaluates the expression following it and then inverts the result.

5   Nested Conditionals

Basic Idea

A nested conditional is an if statement inside another if statement. The inner conditional only runs if the outer conditional's branch ran. If the outer branch was skipped, the inner conditional is also skipped — it does not matter what the inner condition would have evaluated to.

Indentation is the key. Each level of nesting is one additional level of indentation. When you are tracing and you see a deeper indent, that code only runs if all the conditions above it (at shallower indents) were True.

At a Deeper Level

Here is a program with nested conditionals. Trace it with score = 85 and submitted = True:

score = 85
submitted = True
if submitted:
    if score >= 90:
        result = "Distinction"
    elif score >= 70:
        result = "Merit"
    else:
        result = "Pass"
else:
    result = "Not submitted"
print(result)
LinescoresubmittedresultOutput
score = 8585   
submitted = True True  
if submitted:True → True. Take the if branch. Skip outer else.
    if score >= 90:85 >= 90 → False. Move to inner elif.
    elif score >= 70:85 >= 70 → True. Take this branch. Skip inner else.
        result = "Merit"  "Merit" 
print(result)   Merit

Now trace the same program with submitted = False. The outer condition is False, so the entire inner block — all four conditions inside it — is skipped. None of them run.

LinescoresubmittedresultOutput
score = 8585   
submitted = False False  
if submitted:False → False. Skip entire if block. Take outer else.
    result = "Not submitted"  "Not submitted" 
print(result)   Not submitted

Notice that the indented result = "Not submitted" here is inside the outer else block — not inside the inner conditionals. The indentation in the table reflects the indentation in the code.

Track which level you are at before evaluating

When a program has nested conditionals, before evaluating any condition, check which level it is at. If the outer condition was False, every condition inside that block is unreachable — do not evaluate them. Write one decision row for the outer condition that says the entire block is skipped, and move on to whatever comes after the block.

At the Deepest Level

Deep nesting is widely considered a design problem in professional code. A function with four levels of nested conditionals is hard to read, hard to test, and hard to debug — not because indentation is cosmetically unpleasant, but because the number of distinct execution paths grows exponentially with each level of nesting. Two nested conditionals produce up to four paths. Three produce up to eight. Four produce up to sixteen.

Compound conditions with and are often a cleaner alternative to one level of nesting. The two programs below are logically equivalent:

# Nested version
if submitted:
    if score >= 70:
        result = "Pass"

# Compound version
if submitted and score >= 70:
    result = "Pass"

The compound version is shorter, flatter, and easier to trace. In Grade 9, both are acceptable. As programs grow more complex, preferring flat structures over nested ones becomes increasingly important for maintainability.

6   A Common Trap: Independent ifs vs elif

Basic Idea

Two separate if statements are not the same as an if/elif. A separate if is always evaluated, regardless of what came before. An elif is only evaluated if all previous conditions in the chain were False.

This difference causes bugs that are easy to miss when reading but obvious when tracing.

At a Deeper Level

Here are two programs that look almost identical. Trace both with score = 95 and find the difference.

Version A — using elif:

score = 95
label = ""
if score >= 90:
    label = "high"
elif score >= 50:
    label = "medium"
print(label)

Version B — using two separate ifs:

score = 95
label = ""
if score >= 90:
    label = "high"
if score >= 50:
    label = "medium"
print(label)

Tracing Version A:

LinescorelabelOutput
score = 9595  
label = "" "" 
if score >= 90:95 >= 90 → True. Take this branch. Skip elif.
    label = "high" "high" 
print(label)  high

Tracing Version B:

LinescorelabelOutput
score = 9595  
label = "" "" 
if score >= 90:95 >= 90 → True. Take this branch.
    label = "high" "high" 
if score >= 50:95 >= 50 → True. Take this branch. This is a separate if — it always runs.
    label = "medium" "medium" 
print(label)  medium

Version A prints high. Version B prints medium. In Version B, both conditions are true for a score of 95, so both branches run — and the second one overwrites the first. The trace makes this completely visible. Without tracing, the bug can be very hard to spot by reading alone.

Separate ifs are independent decisions; elif chains are one decision

Use elif when the conditions are mutually exclusive — when only one outcome should be possible. Use separate if statements when each condition is genuinely independent and multiple outcomes can occur at the same time. Getting this wrong is one of the most common logic bugs in beginner programs, and it is reliably caught by tracing.

At the Deepest Level

This distinction between chained and independent conditions is an example of a broader design question: are these conditions mutually exclusive (at most one can be true), exhaustive (at least one must be true), both, or neither?

An if/elif/else chain is both mutually exclusive and exhaustive — exactly one branch always runs. An if/elif without an else is mutually exclusive but not exhaustive — it is possible that nothing runs. Two separate if statements are neither mutually exclusive nor exhaustive. Recognising which structure a problem requires is a design skill that develops with practice. Tracing is the tool that reveals whether you chose the right structure.

Check Your Understanding
  1. Trace this program with x = 15. Write a full decision row for the condition, then continue the trace.

    x = 15
    if x % 2 == 0:
        kind = "even"
    else:
        kind = "odd"
    print(kind)

    Now trace again with x = 8. What changes in the table?

  2. Trace this elif chain with speed = 85. Write a separate decision row for every condition that is evaluated, including the ones that are False.

    speed = 85
    if speed > 120:
        warning = "dangerous"
    elif speed > 100:
        warning = "fast"
    elif speed > 80:
        warning = "moderate"
    else:
        warning = "safe"
    print(warning)
  3. Trace this program with logged_in = False and is_admin = True. Before you start, state which branches will and will not run.

    logged_in = False
    is_admin = True
    if logged_in and is_admin:
        print("Admin dashboard")
    elif logged_in:
        print("User dashboard")
    else:
        print("Please log in")
  4. A student traces a program with a compound condition and writes just "False" in the decision row. What information is missing, and why does it matter?
  5. Here are two programs. Without tracing, predict whether they will produce the same output for all possible values of n. Then trace both with n = 5 and n = 15 to check your prediction.

    # Program 1
    n = 5
    if n > 10:
        label = "big"
    elif n > 3:
        label = "medium"
    else:
        label = "small"
    print(label)
    
    # Program 2
    n = 5
    if n > 10:
        label = "big"
    if n > 3:
        label = "medium"
    if n <= 3:
        label = "small"
    print(label)
  6. Trace this nested conditional with age = 20 and member = False. Then trace it again with age = 20 and member = True.

    age = 20
    member = False
    if age >= 18:
        if member:
            price = 8
        else:
            price = 12
    else:
        price = 5
    print("Ticket:", price)