Python Part I · Module 9 · Conditional Statements
Nested and Compound Conditions
Some questions need two decisions rather than one. Did the run finish, and if it did, was it clean? The second question only makes sense once the first is answered, and that shape — a decision inside a decision — is what nesting expresses. It is also the shape most often pointed at as something to simplify, usually with the advice to combine the two conditions with and. That advice is correct, and it is correct under a condition nobody states: only when the outer if has no else. This chapter shows both forms working, then shows the run where the flattened one reports something that is simply untrue, and settles what to write instead.
Foundation15 min readPythonPart IFundamentalsConditionalsNesting
Part I · Module 9 · Chapter 9.4 · Nested and Compound Conditions
1. A Decision Inside a Decision
A branch is a block, and a block may contain anything — including another if.
status = "FAIL"
rerun = True
if status == "FAIL":
if rerun:
print("rerunning the test")
else:
print("investigate the failure")
else:
print("continue to the next stage")rerunning the testRead it as two questions asked in order. Did it fail? Yes, so enter the outer block. Are reruns enabled? Yes, so take the inner if.
The second question is only asked when the first is answered yes. That is the defining property of a nest: the inner decision is reachable only through the outer one. There is no sense in asking whether to rerun a test that passed.
Chapter 9.2 already gave you everything needed to read this. The inner if is at column 4, so it is inside the outer branch; its own blocks are at column 8; the else at column 0 belongs to the outer if because it aligns with it.
2. Nesting Is Not Special
It is worth saying plainly that nothing new was introduced. An if inside an if is an if in a block, and a block can hold any statement. The rules from the last three chapters apply unchanged at every level:
- the condition is evaluated, producing a truth value
- the first true branch of each chain wins
elsemeans none of the preceding alternatives at this level matched- the column says what each line belongs to
The only thing nesting adds is that a reader must now hold more than one answered question in their head at once — which is the entire cost, and the subject of Section 6.
3. Two Conditions, One Decision
Often the inner decision is not really a second question. It is the same question with two requirements:
finished = True
errors = 0
if finished:
if errors == 0:
status = "PASS"
print(status)PASSNothing happens between the two if lines. The outer branch exists purely to reach the inner one, and there is no else on either. When that is the case, the two conditions can be combined:
finished = True
errors = 0
if finished and errors == 0:
status = "PASS"
print(status)PASSOne level instead of two, one condition that reads as one sentence — finished and clean — and the same behaviour. This is the standard advice, and here it is right.
4. When Flattening Changes the Answer
Now add an else to the outer if, and the equivalence quietly stops holding.
Here is the nested version with an outer else, for a run that finished and reported four errors:
finished = True
errors = 4
if finished:
if errors == 0:
print("PASS")
else:
print("did not finish")
print("(end)")(end)Nothing was reported about the outcome. The outer condition was true, so the else was not taken; the inner condition was false, and the inner if has no else, so nothing printed. That is incomplete — but it is not false.
Now the flattened version, same data:
finished = True
errors = 4
if finished and errors == 0:
print("PASS")
else:
print("did not finish")
print("(end)")did not finish
(end)The run finished. The program said it did not.
The else is the problem. In the nested form, else attaches to finished alone and means the run did not finish. In the flattened form it attaches to the whole combined condition and now means the run did not finish or it had errors — two different situations reported with one sentence written for only the first.
5. Three Outcomes Need Three Branches
Neither version above is right, because the data has three cases and both programs offered two. Say so directly:
finished = True
errors = 4
if not finished:
print("did not finish")
elif errors == 0:
print("PASS")
else:
print("FAIL")FAILOne level, three branches, each with its own sentence. The nesting is gone and nothing was lost, because the nest was never expressing two questions — it was expressing three outcomes badly.
Check it against every case:
finished | errors | not finished | errors == 0 | Reported |
|---|---|---|---|---|
| False | 0 | True | — | did not finish |
| False | 4 | True | — | did not finish |
| True | 0 | False | True | PASS |
| True | 4 | False | False | FAIL |
Four input combinations, four correct answers, no gaps. The dashes are conditions that were never evaluated, because the first branch already matched — Chapter 9.1's rule again.
6. What Nesting Costs
Nesting is valid and sometimes necessary. It is also the construct that most reliably makes code hard to check, and the reason is mechanical rather than aesthetic.
status = "FAIL"
rerun = True
budget = 0
if status == "FAIL":
if rerun:
if budget > 0:
print("rerunning")
else:
print("no budget to rerun")
else:
print("investigate")
else:
print("continue")no budget to rerunTo know when "no budget to rerun" appears, you must hold three answers at once: the status failed, and reruns are on, and the budget is zero. To know when "investigate" appears you must hold two, one of which is a negative. Each level roughly doubles the number of paths and adds one fact the reader must carry to the bottom of the block.
Three levels is near the practical limit for code that can be checked by reading. Beyond it, the else branches are the ones that go wrong, because an else four levels deep is defined entirely by what it is not.
7. Three Ways to Reduce a Level
All three use only what this module has taught.
Combine, when there is no else — Section 3's case:
finished = True
errors = 0
if finished and errors == 0:
print("PASS")PASSTurn outcomes into a chain, when the nest was really enumerating cases — Section 5's case.
Invert the outer condition when the nest exists only to exclude something:
tests = ["smoke", "reset"]
name = "burst"
if name not in tests:
print("not in the suite")not in the suiteThat is one level where a nest of if it is a known suite, then if the name is absent would have been two, and it says the same thing.
What none of these can do is remove a genuine two-question structure, where something happens between the questions or each level has its own else. Section 1's example is that shape, and it should stay nested.
8. The Nest That Must Stay: Guarding a Lookup
Here is the case where the inner decision is genuinely unreachable without the outer one, and where removing the guard is an error rather than a style choice.
Chapter 8.3 established that a missing key raises. Watch it happen inside a condition:
status = {"smoke": "PASS"}
if status["reset"] == "FAIL":
print("reset failed")KeyError: 'reset'The lookup ran before the comparison could, and there is no "reset" entry. Guard it:
status = {"smoke": "PASS"}
if "reset" in status:
if status["reset"] == "FAIL":
print("reset failed")
else:
print("reset did not fail")
else:
print("reset has no recorded result")reset has no recorded resultThree genuinely different situations, three messages: no recorded result, recorded and failing, recorded and not failing. The nest is doing real work — status["reset"] is only safe because the outer condition was true.
And when the middle case does not need its own message, the flat form is available after all, for exactly the reason Section 3 gave:
status = {"smoke": "PASS"}
if "reset" in status and status["reset"] == "FAIL":
print("reset failed")
else:
print("not a recorded reset failure")not a recorded reset failureThis is safe — and it is safe only because and short-circuits. If Python evaluated both sides, the lookup would raise exactly as the unguarded version did. The guard must be on the left.
9. Predict, Run, Observe
Four programs. Two differ from each other only in indentation or in and. Predict all four before reading on.
# A
finished = True
errors = 0
if finished:
if errors > 0:
print("A: errors")
print("A: checked")
print("A: end")
# B
ready = False
armed = True
if ready and armed:
print("B: go")
else:
print("B: hold")
# C
status = {"smoke": "PASS"}
if "smoke" in status and status["smoke"] == "PASS":
print("C: smoke passed")
else:
print("C: no")
# D
status = {"smoke": "PASS"}
if status.get("reset") == "FAIL":
print("D: reset failed")
else:
print("D: not a recorded failure")The verified output:
A: checked
A: end
B: hold
C: smoke passed
D: not a recorded failureA — the inner condition is false so A: errors is skipped, but A: checked is at column 4, inside the outer branch, so it runs. Column, not proximity.
B — ready is false, so and stops there and the else runs.
C — the guard is true, so the lookup happens and is safe; the comparison is true as well.
D — .get() returns None for a missing key rather than raising, and None == "FAIL" is false. No guard needed because .get() is the guard — Chapter 8.3's alternative, doing the same job in one expression.
10. Exercises
Exercise 1 — read the columns. For program A in Section 9, say which block each of the three print calls belongs to, and give the output for finished = False.
Exercise 2 — flatten safely. Which of these may be flattened into a single and condition without changing behaviour? For each that may not, say what would break.
# (a)
if a:
if b:
print("x")
# (b)
if a:
if b:
print("x")
else:
print("y")
# (c)
if a:
print("entering")
if b:
print("x")Exercise 3 — find the false statement. Using Section 4's flattened program, find an input pair for which the printed sentence is factually wrong, and one for which it is right. Explain the difference in two sentences.
Exercise 4 — complete the cases. Section 4's nested version prints nothing for finished = True, errors = 4. Rewrite it as a chain so every input prints exactly one message, then build a four-row table like Section 5's to prove it.
Exercise 5 — guard the lookup. widths = {"clk": 1, "data": 32}. Write a program that prints the width of "addr" if it is recorded, and "addr is not in the table" otherwise. Write it twice — once with a guarded condition, once with .get() — and say which you prefer and why.
Exercise 6 — the order matters. Take Section 8's flat guarded condition and swap the two halves of the and. Predict what happens, then run it and record the exact error. Explain in one sentence which Chapter 9.3 rule this demonstrates.
Exercise 7 — reduce the depth. Rewrite Section 6's three-level program so that no branch is deeper than one level, using only chains and compound conditions. Verify that your version gives the same answer for all three of: ("FAIL", True, 0), ("FAIL", False, 5) and ("PASS", True, 5).
Exercise 8 — design the shape. A test may be skipped, may fail, or may pass; a failing test may or may not be on the known-issues list. Decide how many messages this needs, then write the conditional. Say in one sentence why you chose a nest or a chain.
11. Summary
- An
ifinside anifis just anifin a block. Nesting introduces no new rule; the column still says what each line belongs to. - A nest expresses a question that is only asked once another is answered, which is why the inner decision is unreachable except through the outer one.
- Two conditions with no
elseand nothing between them flatten into oneand, and short-circuit evaluation is what keeps that equivalent. - Flattening is not safe when the outer
ifhas anelse. Theelsethen covers a wider set of inputs, and the message written for it becomes false for some of them — with no error. - Count the outcomes, not the conditions. Three things to say want three branches at one level, not a nest.
- Each level of nesting adds a fact the reader must carry and roughly doubles the paths; three levels is the practical reading limit.
- Three remedies are available now: combine with
and, turn outcomes into a chain, or invert the outer condition withnot. - A guard around a dictionary lookup is a nest that must stay — or a flat
andwith the guard on the left, which works only becauseandshort-circuits. .get()remains the alternative when a default value is wanted instead of a different action.
The next chapter asks what Python does when the condition is not a comparison at all — when it is just a number, a string, or a list.
Where this fits
Part of the Python curriculum.
