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.
| Level | What it covers | When to read it |
|---|---|---|
| Basic idea | How to trace a simple if/else; how to record the decision in a trace table | Right now. |
| At a deeper level | elif chains, nested conditionals, and compound conditions with and/or/not | Once you have traced a simple if/else on your own. |
| At the deepest level | How Python evaluates conditions; short-circuit evaluation; truthiness | When the basic and deeper levels feel solid and you want to understand what Python is actually doing. |
1 The Core Rule
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:
- A decision row — you write the condition, substitute the current variable values, evaluate it, and state which branch you are taking.
- 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.
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.
| Line | temperature | message | Output |
|---|---|---|---|
temperature = 35 | 35 | ||
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.
| Line | temperature | message | Output |
|---|---|---|---|
temperature = 20 | 20 | ||
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.
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.
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
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:
- Evaluate the first condition. If it is
True, take that branch and skip everything else in the chain. - If it is
False, move to the firstelifand evaluate its condition. - Continue until a condition is
Trueor the chain is exhausted. - If an
elseexists and nothing wasTrue, take theelsebranch.
Only one branch ever runs, no matter how many conditions you have.
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)| Line | score | grade | Output |
|---|---|---|---|
score = 73 | 73 | ||
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.
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.
| Line | score | grade | Output |
|---|---|---|---|
score = 55 | 55 | ||
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 | ||
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
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.
| Line | score | bonus | Output |
|---|---|---|---|
score = 45 | 45 | ||
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.
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.
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
A compound condition combines two or more comparisons into a single condition using the keywords and, or, and not.
| Keyword | What it means | Result is True when... |
|---|---|---|
and | Both parts must be True | The left side is True and the right side is also True |
or | At least one part must be True | The left side is True or the right side is True (or both) |
not | Reverses the result | The 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.
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:
| Line | age | has_ticket | Output |
|---|---|---|---|
age = 17 | 17 | ||
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:
| Line | age | has_ticket | Output |
|---|---|---|---|
age = 19 | 19 | ||
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.
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.
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
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.
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)| Line | score | submitted | result | Output |
|---|---|---|---|---|
score = 85 | 85 | |||
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.
| Line | score | submitted | result | Output |
|---|---|---|---|---|
score = 85 | 85 | |||
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.
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.
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
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.
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:
| Line | score | label | Output |
|---|---|---|---|
score = 95 | 95 | ||
label = "" | "" | ||
if score >= 90: | 95 >= 90 → True. Take this branch. Skip elif. | ||
label = "high" | "high" | ||
print(label) | high | ||
Tracing Version B:
| Line | score | label | Output |
|---|---|---|---|
score = 95 | 95 | ||
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.
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.
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.
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?Trace this
elifchain withspeed = 85. Write a separate decision row for every condition that is evaluated, including the ones that areFalse.speed = 85 if speed > 120: warning = "dangerous" elif speed > 100: warning = "fast" elif speed > 80: warning = "moderate" else: warning = "safe" print(warning)Trace this program with
logged_in = Falseandis_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")- 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?
Here are two programs. Without tracing, predict whether they will produce the same output for all possible values of
n. Then trace both withn = 5andn = 15to 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)Trace this nested conditional with
age = 20andmember = False. Then trace it again withage = 20andmember = True.age = 20 member = False if age >= 18: if member: price = 8 else: price = 12 else: price = 5 print("Ticket:", price)