This lesson introduces the if statement in all four of its forms, explains why only the first true branch of a chain runs, and shows the two ways to flatten a nested conditional: logical operators and Python's chained comparison.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 5 — Conditionals and recursion
§5.4-5.7, pp. 41-42
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-42 — the pages these objectives are drawn from
Warm-up
Everything you have written so far does the same thing every time. Work out what is missing.
Discussion prompt
Name a program you have used that behaves differently depending on what it finds — different input, a different file, a different time of day. What does it have to be able to do that none of your programs so far can?
Hint: Lesson 1a named this as one of the five kinds of instruction.
Answer:
It has to check a condition and run different code depending on the answer. That is conditional execution, the fourth of the five categories from lesson 1a.
In order to write useful programs, we almost always need the ability to check conditions and change the behavior of the program accordingly. Conditional statements give us this ability.
Everything in the previous lesson was building the conditions. This lesson is the statement that acts on them.
Concept
The simplest form of conditional statement is the if statement: the keyword if, a boolean expression called the condition, a colon, and an indented body. If the condition is true, the body runs. If not, nothing happens.
condition — The boolean expression in an if statement, which decides whether the body runs.
if statements have the same structure as function definitions: a header followed by an indented body. Statements like this are called compound statements — and you have now met three of them, since the for statement in chapter 4 has the same shape.
Figure (svg): A flow chart showing a condition being tested, with the body running only when it is true
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-41
Section
Section 1
Concept
The boolean expression after if is called the condition. If it is true, the indented statement runs. If not, nothing happens — the body is skipped and the program continues after it.
x = 5
if x > 0:
print('x is positive')
print('done')| Line | What happens | Output |
|---|---|---|
| x = 5 | assignment | x -> 5 |
| if x > 0: | the condition is True | run the body |
| print(...) | indented, so it is the body | x is positive |
| print('done') | not indented, so it always runs | done |
There is no limit on the number of statements that can appear in the body, but there has to be at least one. Occasionally it is useful to have a body with no statements, usually as a place keeper for code you have not written yet — and for that there is the pass statement, which does nothing.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-41
Picture it
Two print statements, and only one of them depends on the condition.
Figure (svg): Two columns showing an indented statement inside the if body and an un-indented statement after it
This is the third compound statement you have met — def, for and now if — and all three have exactly the same shape. Learning it once covers all of them.
Worked example
Predict the output before advancing. The interesting part is what does NOT happen.
x = -3
if x > 0:
print('x is positive')
print('done')| Line | What happens | Output |
|---|---|---|
| x = -3 | assignment | x -> -3 |
| if x > 0: | -3 is not greater than 0 | condition is False |
| print(...) | skipped entirely | no output |
| print('done') | not part of the body | done |
Evaluate the condition.
Why: It is an ordinary boolean expression, evaluated exactly as it would be at the prompt. Here it produces False.
Skip the body.
Why: If the condition is not true, nothing happens — the body is not run, and no error occurs. A false condition is a completely normal outcome.
Continue after the statement.
Why: The un-indented print is not part of the body, so it runs regardless.
Figure (svg): The state of the program after each line of Worked example a condition that is false, drawn as a ladder with one rung per traced line
One line of output: done. The conditional print was skipped because the condition was False, and the program continued normally afterwards.
Verify: Change x to 5 and check that exactly one more line appears.
Why: Two lines instead of one. The un-indented print appears in both runs and the indented one only in the second, which confirms which statement the condition actually controls — the fastest way to check an indentation you are unsure about.
Prediction
Only one of the two print statements is conditional.
x = 0
if x > 0:
print('positive')
print('and that is that')
print('finished')| Part | What happens | Output |
|---|---|---|
| x > 0 | 0 is not greater than 0 | False |
| both indented lines | skipped together | no output |
| print('finished') | not indented | finished |
Predict first
How many lines of output?
Correct: One — both indented statements are skipped, and only the un-indented one runs.
Why: The body is everything indented under the header, which here is two statements, and a false condition skips all of them together. The tempting answer is two, from thinking that only the first indented line belongs to the if. The body can contain any number of statements, and the indentation is what says which — exactly as it does for a function or a loop.
Worked example
A body must have at least one statement. Sometimes you have nothing to put there yet.
if x < 0:
pass # TODO: need to handle negative values!| Line | What it does | Effect |
|---|---|---|
| if x < 0: | the condition is evaluated as usual | True or False |
| pass | a statement that does nothing | no effect |
| the comment | a note to the reader | ignored by Python |
Understand why an empty body is illegal.
Why: There has to be at least one statement in the body. An if header with nothing indented under it is an IndentationError — Python expected a block and found none.
Use pass as the placeholder.
Why: It is a statement that does nothing, which satisfies the requirement without doing anything you did not intend.
Note the comment.
Why: The book pairs pass with a TODO comment, which is the honest use of it: a marker that this case is known about and not yet handled.
Figure (svg): A panel showing the error produced by an if statement with no body beside the version using pass
A legal if statement whose body does nothing. pass exists precisely for this, usually as a place keeper for code you have not written yet.
Verify: Delete the pass and run it.
Why: An IndentationError: expected an indented block. The error confirms that the requirement is real and that pass is genuinely doing something — namely satisfying the parser without affecting the program.
Trap
A student writes if x > 0 with no colon and gets a syntax error pointing at the end of the line.
Treat the colon as punctuation you can leave off
Why: The line reads perfectly well without it, and English does not require one.
The colon is what tells Python the header has ended and a block follows. Without it, Python is still waiting for the rest of the header when the line runs out.
Every compound statement header ends with a colon.
Learn it once for all three
Why: def, for and if all end their header with a colon and indent their body. There is no exception.
Read the colon as and here is what to do
Why: It separates the question from the answer, which is what makes it worth a character.
This is the same shape you learned in lesson 3b for function definitions. Meeting it a third time is a good moment to stop thinking of it as a rule about if statements and start thinking of it as the rule about compound statements.
Sorting
Indentation is the only thing that decides.
Sort into buckets
Given if x > 0: with x equal to -1, which of these lines run?
Faded example
A header and a body. Two blanks, both required characters.
Fill in the blanks
if x < 0:
print('that is negative')
Why: The colon ends the header and the indented line is the body. Both are required rather than conventional: without the colon Python reports a syntax error, and without an indented statement beneath it Python reports that it expected an indented block. The four spaces of indentation are supplied here because they are the convention — Python accepts any consistent amount, and four is what everybody uses.
Socratic
Python could have allowed an empty block. Work out why it does not.
Discussion prompt
Why might a language require an if statement to have at least one statement in its body, rather than allowing an empty one — and what does the existence of pass tell you about that decision?
Hint: Think about what an empty body would look like on the page.
Answer:
Because the block is defined by indentation, an empty body would be invisible: there would be nothing to indent, so nothing would show that a block was intended, and the next line would look like the body.
Requiring a statement means the structure is always visible, which is the same reason the indentation rule exists at all.
The existence of pass tells you the designers knew the requirement was sometimes inconvenient, and chose to supply an escape hatch rather than relax the rule. That is a common trade: keep the rule strict so it is reliable, and provide one explicit way to opt out.
Section
Section 2
Concept
A second form of the if statement is alternative execution, in which there are two possibilities and the condition determines which one runs.
branch — One of the alternative paths through a conditional statement.
if x % 2 == 0:
print('x is even')
else:
print('x is odd')| Case | The condition | Output |
|---|---|---|
| x = 4 | 4 % 2 == 0 is True | x is even |
| x = 7 | 7 % 2 == 0 is False | x is odd |
| either way | exactly one branch runs | always one line of output |
Since the condition must be true or false, exactly one of the alternatives will run. The alternatives are called branches, because they are branches in the flow of execution.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-41
Picture it
Two paths, one taken, and they come back together afterwards.
Figure (svg): A flow chart showing a condition splitting into two branches which rejoin after the conditional statement
The word branch is worth taking literally: it is a fork in the flow of execution, and unlike a function call the two paths do not both happen.
Worked example
The book's own example, and it uses the modulus test from the previous lesson.
x = 7
if x % 2 == 0:
print('x is even')
else:
print('x is odd')| Step | What happens | Result |
|---|---|---|
| x % 2 | seven divided by two leaves one | 1 |
| 1 == 0 | the comparison | False |
| else branch | the condition was false | x is odd |
Evaluate the condition using both layers.
Why: Modulus produces a number, the comparison turns it into a bool. Both steps from the previous lesson are needed.
Take the branch the answer selects.
Why: If the remainder when x is divided by 2 is 0 then x is even; the condition being false means it is odd.
Notice that no third outcome exists.
Why: The condition must be true or false, so exactly one of the alternatives runs. There is no way for both to run and no way for neither to.
Figure (svg): The state of the program after each line of Worked example even or odd, drawn as a ladder with one rung per traced line
One line: x is odd. The condition was False, so the else branch ran — and exactly one branch always runs, so there is always exactly one line of output.
Verify: Run it for an even number and check the output count.
Why: Still one line, now saying even. If a change to the input ever produced zero lines or two, the structure would be wrong rather than the condition — which makes the output count a check on the shape of the statement rather than on the logic.
Prediction
The boundary case is where most conditions go wrong.
x = 0
if x > 0:
print('positive')
else:
print('not positive')| Step | Why | Output |
|---|---|---|
| x > 0 | 0 is not greater than 0 | False |
| else branch | runs whenever the condition is false | not positive |
Predict first
What does this print when x is 0?
Correct: not positive — zero is not greater than zero, so the condition is False and the else branch runs.
Why: The boundary is the case worth checking on every condition you write, because it is where a greater-than and a greater-than-or-equal-to differ. Note also that exactly one line appears: an if-else always produces exactly one branch's worth of output, which is a useful structural check.
Worked example
A common misreading. Find out what else actually tests.
if x > 0:
print('positive')
else:
print('not positive')| Case | The condition | Which branch |
|---|---|---|
| x = 5 | condition True | positive |
| x = -5 | condition False | not positive |
| x = 0 | condition False | not positive |
Notice that else has no condition of its own.
Why: It is not else if x is negative. It runs whenever the if condition was false, whatever the reason.
Check the boundary case.
Why: Zero is not greater than zero, so the condition is False and the else branch runs. Calling that branch negative would be wrong.
Name the branch honestly.
Why: The else branch covers everything the condition excluded, so not positive is accurate and negative is not.
Figure (svg): Two columns showing which values take the if branch and which take the else branch, including zero
The else branch runs for every value that fails the condition, including zero. It has no condition of its own, which is exactly why exactly one branch always runs.
Verify: Look for a value that takes neither branch.
Why: There is none. Every value either satisfies x > 0 or does not, and the two branches cover both cases exhaustively. That exhaustiveness is what distinguishes if-else from two separate if statements, which need not cover everything.
Trap
A student writes if x > 0 and then a second, separate if x <= 0, believing it is the same as if-else.
Reason that the two conditions are opposites, so exactly one will hold
Why: Which is true — for the values considered when the code was written.
Then one condition is edited and the pair stops being exhaustive, or a value satisfies both, and now zero or two branches run where exactly one was intended. Nothing warns you.
Use else when you mean everything the condition excluded.
Write one condition and let else cover the rest
Why: The two branches are then guaranteed exhaustive and mutually exclusive, by construction rather than by care.
Reserve separate ifs for genuinely independent tests
Why: Two questions that could both be true, or both false, deserve two if statements.
This is the same reasoning that will decide between elif and separate ifs in the next section, and it is the most common structural mistake in conditional code.
Discrimination
Ask whether the two cases are opposites of one question, or two questions.
Sort into buckets
For each pair of checks, which structure is right?
Fill the middle
One condition, two branches, and a keyword that takes no condition of its own.
Fill in the blanks
if n > 100:
print('big')
else:
print('not big')
Why: else takes no condition — it runs whenever the if condition was false — which is why it is followed directly by a colon. Writing else n <= 100: would be a syntax error, and it would also be redundant: the whole point of else is that it covers everything the condition excluded, so restating the opposite condition adds nothing and creates a chance for the two to disagree.
Explain it to yourself
The guarantee is worth understanding rather than memorising.
Discussion prompt
Explain why an if-else statement always runs exactly one of its two branches — never zero and never both — using what you know about boolean expressions.
Hint: How many values does a condition have?
Answer:
Because the condition must be true or false, and there is no third possibility. A boolean expression has exactly two possible values, so the split it produces is exhaustive.
Never both, because the else branch runs only when the condition was false, and it cannot be both true and false at once.
This is a genuinely strong guarantee, and it is what makes if-else safer than two separate ifs: the exhaustiveness comes from the type having two values, not from the programmer having thought of every case.
Section
Section 3
Concept
Sometimes there are more than two possibilities and you need more than two branches. One way to express a computation like that is a chained conditional, using elif — an abbreviation of else if.
if x < y:
print('x is less than y')
elif x > y:
print('x is greater than y')
else:
print('x and y are equal')| Branch | When it is checked | What happens |
|---|---|---|
| x < y | checked first | if true, print and stop |
| x > y | checked only if the first was false | if true, print and stop |
| else | reached only if both were false | they must be equal |
Again, exactly one branch will run. There is no limit on the number of elif statements. If there is an else clause it has to be at the end, but there does not have to be one.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-42
Picture it
Each condition is only reached if every condition above it failed.
Figure (svg): A flow chart showing three conditions checked in sequence, each one only reached if the previous failed
The single most important consequence: even if more than one condition is true, only the first true branch runs. That is what makes a chain different from a sequence of separate ifs.
Worked example
Three possibilities, and the third needs no condition at all.
x = 3
y = 3
if x < y:
print('x is less than y')
elif x > y:
print('x is greater than y')
else:
print('x and y are equal')| Branch | The test | Outcome |
|---|---|---|
| x < y | 3 < 3 | False — check the next |
| x > y | 3 > 3 | False — fall through to else |
| else | no condition | x and y are equal |
Check the conditions in order.
Why: Each condition is checked in order. If the first is false, the next is checked, and so on.
Notice why the else needs no test.
Why: If neither less-than nor greater-than holds, the two must be equal. The else branch is the everything remaining case, and here that remainder happens to be exactly one situation.
Notice what did not have to be written.
Why: There is no x == y test anywhere. Writing one would be correct and redundant, and it would create the possibility of a fourth case that never runs.
Figure (svg): The state of the program after each line of Worked example a chain with three outcomes, drawn as a ladder with one rung per traced line
x and y are equal. Both conditions failed, so the else branch ran — and it needed no condition because the two tests above it had already excluded everything else.
Verify: Check that the three branches are exhaustive.
Why: Any two numbers are less than, greater than, or equal — there is no fourth relationship. The chain covers all three cases with two tests, which is the economy an else buys.
Prediction
Two conditions are true. The order decides.
score = 95
if score > 50:
print('pass')
elif score > 90:
print('distinction')| Condition | Value | What happens |
|---|---|---|
| score > 50 | 95 is above 50 | True — this branch runs |
| score > 90 | would be True too | never checked |
| output | pass | distinction is unreachable |
Predict first
What does this print for a score of 95?
Correct: pass — the first condition is true, so the chain stops there and the distinction branch is never reached.
Why: The distinction branch is unreachable for every score, because any score above 90 is also above 50 and the first condition catches it. Swapping the two branches fixes it: check the more specific condition first. Note that Python gives no warning at all about the unreachable branch, which is why ordering is something you have to reason about rather than rely on being told.
Worked example
Two conditions are both true. Predict which branch runs.
n = 12
if n % 2 == 0:
print('divisible by 2')
elif n % 3 == 0:
print('divisible by 3')| Condition | Its value | What happens |
|---|---|---|
| n % 2 == 0 | 12 is even | True — this branch runs |
| n % 3 == 0 | 12 is also divisible by 3 | never checked |
| output | one line only | divisible by 2 |
Evaluate the first condition.
Why: Twelve is even, so it is True and the first branch runs.
Notice that the second condition is never evaluated.
Why: If one of them is true, the corresponding branch runs and the statement ends. The second condition is not merely ignored — it is never tested at all.
State the rule.
Why: Even if more than one condition is true, only the first true branch runs.
Figure (svg): Two columns comparing a chain of elif branches with two separate if statements for a value satisfying both conditions
One line: divisible by 2. Twelve is divisible by three as well, but the chain stopped at the first true condition and never looked at the second.
Verify: Rewrite it as two separate if statements and compare.
Why: Two separate ifs print both lines, because each is tested independently. The chain prints one. That difference is the entire practical content of the first-true rule, and it is invisible until a value satisfies two conditions.
Trap
A student writes a chain that checks n greater than 0 before n greater than 100, meaning to report large numbers specially.
Order the conditions by how they came to mind
Why: Both are true for 500, and it is easy to assume the more specific one will be chosen.
It is not. For 500 the first condition holds, so the first branch runs and the special case is unreachable — a branch that can never execute, with no warning.
Put the most specific condition first, because the first true branch wins.
Check the more restrictive test earlier
Why: n > 100 before n > 0, so that a large number is caught by the specific branch.
Ask of every branch: could an earlier condition have caught this?
Why: If yes, the branch is unreachable and the order is wrong.
This is the practical consequence of the first-true rule, and it is why the rule is worth stating explicitly rather than discovering. An unreachable branch produces no error and no output — it simply never happens.
Ranking
Most specific first, or the general condition swallows the specific one.
Put in order
Why: Each condition must be more restrictive than the ones below it. A value of 5000 satisfies all three numeric tests, so the huge branch has to be checked first or it can never run. The else goes last, both because the language requires it and because it is the least specific case of all — everything the three tests excluded.
Discrimination
Ask whether the cases are alternatives or independent checks.
Sort into buckets
For each situation, should the second test be an elif or a separate if?
Counterexample
The book says there does not have to be one. Find a case where leaving it out is right.
Discussion prompt
Give an example where a chain of if and elif with NO else is the correct structure, and say what happens for a value that satisfies none of the conditions.
Hint: Think about a case where doing nothing is a legitimate outcome.
Answer:
Warning messages are the clearest case: if the password is too short, warn; elif it has no digit, warn differently. A perfectly good password should produce no warning at all.
For a value satisfying none of the conditions, nothing happens and the program continues. That is not an error — it is the same behaviour as a bare if whose condition was false.
So the else is optional precisely because do nothing is sometimes the right remaining case. Adding an empty else with pass in it would be legal and would say nothing the absence of an else does not already say.
Section
Section 4
Concept
One conditional can be nested within another. The chained example from the previous section could have been written this way instead — and comparing the two is the point.
if x == y:
print('x and y are equal')
else:
if x < y:
print('x is less than y')
else:
print('x is greater than y')| Structure | What it contains | Cost |
|---|---|---|
| outer if | two branches | equal, or everything else |
| inner if | inside the outer else | less than, or greater than |
| indentation | three levels deep | harder to read |
The outer conditional contains two branches. The first contains a simple statement; the second contains another if statement, which has two branches of its own. Although the indentation makes the structure apparent, nested conditionals become difficult to read very quickly. It is a good idea to avoid them when you can.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 42-42
Picture it
Both are correct. One of them is three levels deep.
Figure (svg): Two columns comparing a chained conditional with the equivalent nested version, showing the indentation depth of each
The depth is the problem. Each additional case adds a level to the nested version and no levels at all to the chain, so the difference grows with the number of cases.
Worked example
The book's own example. Two nested ifs become one condition.
# nested
if 0 < x:
if x < 10:
print('x is a positive single-digit number.')
# flattened
if 0 < x and x < 10:
print('x is a positive single-digit number.')| Version | When the print runs | Depth |
|---|---|---|
| nested | the print runs only past both tests | two levels of indentation |
| flattened | the print runs only if both are true | one level |
| equivalent? | yes — and requires both | same behaviour |
See why the two are equivalent.
Why: The print statement runs only if we make it past both conditionals, so we can get the same effect with the and operator.
Notice the condition for flattening.
Why: This works because the inner if is the ONLY thing in the outer body and neither has an else. If either had an else branch, the two forms would differ.
Compare readability.
Why: One condition, one level of indentation, and the whole requirement visible on one line.
Figure (svg): The state of the program after each line of Worked example flattening a nested conditional with and, drawn as a ladder with one rung per traced line
The flattened version is equivalent and one level shallower. Logical operators often provide a way to simplify nested conditional statements, and this is the standard case.
Verify: Test both versions on a value inside and outside the range.
Why: Both print for 5 and both stay silent for 50 and for -3. Checking a value on each side of both boundaries is what confirms the equivalence rather than assuming it — and if either version had an else, one of those tests would have separated them.
Prediction
One has an else. Work out whether the flattening is still valid.
# version A
if a > 0:
if b > 0:
print('both')
else:
print('a is not positive')
# version B
if a > 0 and b > 0:
print('both')| Case | What each version does | Verdict |
|---|---|---|
| a=1, b=1 | A prints 'both'; B prints 'both' | agree |
| a=-1, b=1 | A prints 'a is not positive'; B prints nothing | DISAGREE |
| a=1, b=-1 | A prints nothing; B prints nothing | agree |
Predict first
Are versions A and B equivalent?
Correct: No — B loses the else branch, so for a non-positive a it prints nothing where A printed a message.
Why: The flattening rule requires that the inner if is the only thing in the outer body AND that neither has an else. Here the outer if has one, and it contains behaviour that the flattened version has nowhere to put. Testing a value that takes the else branch is what exposes it — which is why the verification step of a refactoring has to cover every branch, not only the one you were thinking about.
Worked example
For this kind of condition Python provides a more concise option.
if 0 < x and x < 10:
print('x is a positive single-digit number.')
if 0 < x < 10:
print('x is a positive single-digit number.')| Form | When it applies | Note |
|---|---|---|
| 0 < x and x < 10 | the general form | works for any two conditions |
| 0 < x < 10 | the chained form | works only for comparisons on one value |
| equivalent | for this case | and reads as it would be written on paper |
Read the chained form.
Why: It is written exactly as the range would be written in mathematics, with x between the two bounds.
Note the restriction.
Why: It works because both comparisons are about x. It is not a general substitute for and — you cannot chain unrelated conditions this way.
Choose deliberately.
Why: For a range test the chained form is clearer. For anything else, the and form is the one that applies.
Figure (svg): A number line style diagram showing the range from zero to ten with both endpoints excluded
0 < x < 10 means the same as the and version and reads like the mathematics it expresses. It is available specifically for chains of comparisons on the same value.
Verify: Check that it behaves correctly at both boundaries.
Why: For x = 0 and x = 10 neither version prints, because both comparisons are strict. Testing both ends confirms the chained form has the same strictness as the two comparisons it replaces — which is exactly what you would want and worth confirming once.
Trap
A student sees a nested if inside an outer if with an else, and replaces the pair with a single and condition.
Apply the flattening rule without checking its precondition
Why: It worked on the book's example, and the shapes look similar.
The outer else branch has now vanished. Values that used to take it now take no branch at all, and the code that used to run for them silently does not.
Flatten only when the inner if is the whole body and neither has an else.
Check what happens to each existing branch
Why: If the outer else did something, that something needs a home in the flattened version.
Otherwise use a chain instead
Why: if a and b, elif a, else — which is flat and preserves all three outcomes.
The general point: a refactoring is only correct if it preserves behaviour for every input, and an else branch is behaviour for a whole class of inputs. Lesson 4b's verification step applies here exactly.
Faded example
Neither if has an else, so the flattening is valid.
Fill in the blanks
# nested version:
# if age > 12:
# if age < 20:
# print('teenager')
if age > 12 and age < 20:
print('teenager')
Why: The print statement in the nested version runs only if execution gets past both conditions, which is exactly what and requires. Note that this could also be written as the chained comparison 12 < age < 20, which reads even more like the range it describes — both are correct, and the chained form is available because both comparisons are about the same value.
Comparison
Fill the blanks. All three are correct; they differ in depth and readability.
Comparison matrix
| Form | How it looks | When to prefer it |
|---|---|---|
| nested ifs | two headers, two levels | when the branches genuinely differ — otherwise never |
| and | one header, both comparisons written out | any two conditions, related or not |
| chained comparison | 0 < x < 10 | a range test on a single value |
The third row is the most readable and the least general. That is a common trade: a special-purpose form that is clearer exactly where it applies.
Explain it
It is a good idea to avoid them is advice. Give the reason behind it.
Discussion prompt
The book says nested conditionals become difficult to read very quickly. Explain WHY, in terms of what a reader has to hold in their head — and say what makes a chained conditional easier even though it has the same number of cases.
Hint: What do you have to remember while reading line twelve of a nested structure?
Answer:
In a nested structure, understanding any line means remembering every condition above it that led there. At three levels deep, a line is reached only when three separate things are true, and the reader has to hold all three.
A chain is flat: each branch is reached when its own condition is true and the earlier ones were false. That is still information, but it is one condition per branch rather than an accumulating stack of them.
So the cost of nesting grows faster than the number of cases, which is why the advice is about avoiding it when you can rather than never. Sometimes the branches genuinely differ and nesting is honest — but a nested structure that could have been a chain is making the reader work for nothing.
Section
Section 5
Concept
You now have four ways to write a conditional. They are not interchangeable: each expresses a different intention, and choosing the one that matches your intention is what makes the code readable and correct.
The commonest structural bug is using the fourth where the third was meant, or the third where the fourth was. Both compile, both run, and the difference only shows for values that satisfy more than one condition.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-42
Picture it
That single question distinguishes the four forms.
Figure (svg): Two columns comparing forms where exactly one branch runs with forms where any number may run
Asking how many of these should be able to happen before writing anything is the fastest way to get the shape right.
Worked example
Exactly one option should be chosen. Pick the shape that guarantees it.
if choice == 'a':
draw_a()
elif choice == 'b':
draw_b()
elif choice == 'c':
draw_c()| Input | What is checked | What runs |
|---|---|---|
| choice = 'a' | first condition true | draw_a, then done |
| choice = 'b' | first false, second true | draw_b, then done |
| choice = 'z' | all false, no else | nothing happens |
Recognise the intention.
Why: One choice, several options, exactly one to act on. That is a chain.
Notice that the conditions are mutually exclusive anyway.
Why: choice cannot be both 'a' and 'b'. The chain is still right, because it says so explicitly and stops checking once it has an answer.
Notice the missing else.
Why: An unrecognised choice does nothing. If that is not acceptable, an else reporting the problem would go at the end.
Figure (svg): The state of the program after each line of Worked example a menu, done correctly, drawn as a ladder with one rung per traced line
A chain of three branches with no else. Exactly one runs for a recognised choice, and nothing runs for an unrecognised one — which may or may not be what you want, and is worth deciding rather than defaulting into.
Verify: Test with an input that matches nothing.
Why: Nothing happens and no error occurs. Whether that is correct depends on the program, but testing it is how you find out that the case exists — and an unhandled input is the commonest thing a menu forgets.
Prediction
The shape decides, not the conditions.
n = 12
if n % 2 == 0:
print('even')
if n % 3 == 0:
print('divisible by 3')| Statement | What happens | Output |
|---|---|---|
| first if | 12 is even | even |
| second if | a separate statement, tested independently | divisible by 3 |
| total | two independent tests, both true | two lines |
Predict first
How many lines does this print for n = 12?
Correct: Two — these are separate if statements, so both are tested and both conditions hold.
Why: Changing the second if to an elif would produce one line instead, because the chain stops at the first true condition. The conditions are identical in both versions; only the shape differs. That is why the choice between them is a decision about intention rather than about logic.
Worked example
Several warnings, any number of which may apply. Pick the shape that allows that.
if len(password) < 8:
print('too short')
if password == password.lower():
print('no capital letters')
if password.isalpha():
print('no digits')| Password | Which conditions hold | Output |
|---|---|---|
| 'abc' | short, lowercase, all letters | three warnings |
| 'abcdefghij' | long enough, lowercase, all letters | two warnings |
| 'Abc123def' | long enough, has a capital, has digits | no warnings |
Recognise the intention.
Why: Three independent problems, and a password can have all three. Every applicable warning should appear.
Use separate if statements.
Why: Each is tested independently, so any number of them may run — which is exactly what independent checks require.
See what a chain would have done.
Why: It would print only the first applicable warning and hide the others, which for a validation message is actively unhelpful.
Figure (svg): A flow chart showing three independent tests in sequence, each of which may or may not produce output
Three separate if statements, so that all applicable warnings appear. A chain would report only the first problem, and the user would fix it and immediately meet the second.
Verify: Test an input that triggers more than one check.
Why: 'abc' produces three lines. A value satisfying several conditions is the only kind that distinguishes separate ifs from a chain, so it is the test that must be run — and it is exactly the test that a developer with only well-formed test data never performs.
Trap
A developer writes a chain of elifs for a set of checks, tests it on three inputs that each trigger exactly one check, and ships it.
Test with data that does not distinguish the shapes
Why: Every test passed, and the output was correct in each case.
The first input that triggers two checks reports only one of them. The bug was present from the start and no test could have caught it, because no test exercised the distinguishing case.
Choose the shape from the intention, then test the case that distinguishes them.
Ask how many branches should be allowed to run
Why: One, or several. That answer chooses between a chain and separate ifs before any code is written.
Test an input that satisfies more than one condition
Why: It is the only input where the two shapes behave differently, so it is the only test that checks the decision.
This is a general lesson about testing: a test that every candidate implementation passes tells you nothing. The valuable test is the one that separates them.
Sorting
Ask how many branches should be able to run.
Sort into buckets
Sort each requirement by the conditional shape it needs.
Reverse engineer
The conditions are given. The output tells you the shape.
Fill in the blanks
# n = 6, and the output was TWO lines
if n % 2 == 0:
print('even')
if n % 3 == 0:
print('divisible by 3')
Why: Six satisfies both conditions, and two lines appeared, so both branches ran — which only happens with separate if statements. Writing elif would have produced one line, because the chain stops at the first true condition. The output count is enough to determine the shape here, which is a nice example of reasoning backwards from behaviour to structure.
Real world
This decision has consequences well beyond a beginner's program.
Discussion prompt
Think of a real system that gives you feedback on a form or a document — a tax return, a job application, a code checker. Have you ever fixed one error only to be told about a second? What conditional shape were they using, and what would the other shape have felt like?
Hint: The frustrating one reports errors one at a time.
Answer:
Reporting one error at a time is a chain: the first failing check wins and the rest are never tested. Every fix reveals the next problem, which turns one round trip into five.
Separate ifs would collect every failure and report them together, so one round trip is enough. That is nearly always the better experience for validation.
The chain is the right shape when the checks genuinely depend on each other — there is no point complaining about the contents of a file that does not exist. So the decision is about whether the conditions are independent, which is exactly the criterion from this section.
Comparison
Fill the blanks. The right-hand column is the question that chooses between them.
Comparison matrix
| Shape | How many branches run | Use it when |
|---|---|---|
| if alone | zero or one | there is nothing to do in the other case |
| if / else | exactly one | one question with two exhaustive outcomes |
| if / elif / else | exactly one | one question with several mutually exclusive outcomes |
| separate ifs | any number | the questions are independent of each other |
The middle column is the whole difference. Deciding how many branches SHOULD be able to run is what picks the shape.
Pattern
Six steps, and the shape is chosen before any code is written.
Step 6's second half is the one that catches structural bugs. An input satisfying two conditions is the only kind that behaves differently under a chain and under separate ifs.
Python documentation — More Control Flow Tools More Control Flow Tools
Check
Both conditions are true. The shape decides what happens.
n = 10
if n > 5:
print('big')
elif n > 8:
print('very big')| Condition | Value | What happens |
|---|---|---|
| n > 5 | 10 is above 5 | True — this branch runs |
| n > 8 | would also be true | never checked |
| output | one line | big |
Check your understanding
What does this print for n = 10?
Answer: A
Why: Each condition is checked in order and the first true one wins: even if more than one condition is true, only the first true branch runs. Ten is above five, so the first branch runs and the second condition is never evaluated. The very big branch is in fact unreachable for every value, since anything above eight is also above five.
Check
One value tests whether you have read else correctly.
x = 0
if x > 0:
print('positive')
else:
print('negative')| Step | What happens | Note |
|---|---|---|
| x > 0 | 0 is not greater than 0 | False |
| else | runs whenever the condition is false | negative |
| accuracy | zero is not negative | the MESSAGE is wrong |
Check your understanding
What is wrong with this program?
Answer: B
Why: The else branch runs whenever the condition is false, which includes zero — so the program claims zero is negative. The structure is correct and the message is wrong. Fixing it needs a third case: an elif testing x < 0, with the else then covering zero honestly.
Check
One of these is a valid simplification of the nested version.
Check your understanding
Which single condition is equivalent to if a > 0: containing only if b > 0: containing only a print statement?
Answer: B
Why: The print statement runs only if execution gets past both conditions, so both must be true — which is exactly what and requires. This flattening is valid because the inner if is the whole body of the outer one and neither has an else branch.
Real world
The first-true rule is how most rulebooks work, whether or not they say so.
Discussion prompt
Find a set of rules — a fare table, a tax band, a discount policy — where the order in which the rules are checked matters. What would go wrong if the most general rule were listed first, and how does the document make the order clear?
Hint: Tax bands and fare tables both have this problem.
Answer:
Fare tables are the clearest: children travel free; under-25s pay half; everyone else pays full. Checked in that order it works, and reversed it charges children the full fare, because everyone matches the general rule.
Good documents make the order explicit — numbering the rules, or saying the first applicable rule applies. That sentence is exactly Python's first-true rule.
The failure mode is the same too: a rule that can never apply because a broader rule above it always catches the case first. In a document it is a drafting error; in a chain it is an unreachable branch. Neither produces any warning.
Commit first
Answer, then rate your confidence. The two shapes differ only for this kind of input.
Predict first
Two checks, both true for the given input. One version uses if / elif; the other uses two separate ifs. How many lines does each print?
Correct: The chain prints one and the separate ifs print two.
Why: A chain stops at the first true condition and never evaluates the rest, so exactly one branch runs however many conditions would have held. Separate if statements are independent statements, each evaluated in turn, so both run. This is the entire practical difference between the two shapes, and it is invisible for any input that satisfies at most one condition — which is why a test suite of well-behaved inputs can pass while the shape is wrong.
Explain it
The elif-versus-separate-ifs decision is the one worth being able to explain.
Discussion prompt
A classmate has written a form validator using a chain of elifs, and users complain that they fix one error and immediately meet another. Explain what is happening and what to change, in three sentences.
Hint: The fix is one word, repeated.
Answer:
Say: a chain stops at the first true condition, so only the first error is ever reported. The other checks are never evaluated at all.
The fix is to change each elif to a plain if, making them independent statements so that every applicable error is reported.
Then add the reason, because it will keep being useful: use a chain when exactly one case should apply, and separate ifs when the cases are independent. Validation errors are independent — a password can be both too short and missing a digit.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The syntax settles within a few programs, especially since it is the same shape as def and for. The if-else guarantee is a single idea and follows from booleans having two values. The first-true rule is the one worth over-learning, because getting a branch order wrong produces an unreachable branch with no warning at all, and because the chain-versus-separate-ifs choice keeps arising for the rest of your programming life. Flattening is a readability skill, and the important half of it is knowing when the flattening is NOT valid.
Connect it up
One page, from memory.
Draw it
Draw the four conditional shapes as four small flow charts side by side: a bare if, an if-else, a chain, and two separate ifs. Under each, write how many branches can run. Then pick one input that would behave differently under the chain and under the separate ifs, and write beside each chart what that input produces. Finally, mark on the chain diagram where an unreachable branch would appear if the conditions were ordered wrongly.
Recap
Two pages, and your programs can finally do different things on different days.
| If you remember one thing | It is this |
|---|---|
| From the basic if | The indentation decides what is conditional. A body needs at least one statement; pass is one. |
| From else | It has no condition. It covers everything the if excluded, including the boundary case. |
| From chains | Only the first true branch runs, even if several conditions are true. |
| From the shapes | Exactly one branch means a chain; any number means separate ifs. |
| From nesting | Flatten with and only when neither if has an else. |
The next lesson takes the if statement somewhere surprising: a function that calls itself, which needs a condition to stop it — and the keyboard input that finally lets a program ask a question.
Think Python, 2nd edition — Allen B. Downey §5.4-5.7, pp. 41-42 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.