5b Conditional Execution: if, else, elif, and Nesting

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

What this lesson covers

The lesson, slide by slide

1. Lesson 5b Conditional Execution: if, else, elif, and Nesting

Title

Python · Chapter 5 — Conditionals and recursion

§5.4-5.7, pp. 41-42

2. By the end of this lesson you can

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

3. Before we start: what does a program need in order to choose?

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.

4. The one idea behind this lesson: a header, a condition, a body

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

5. The if statement: run this, sometimes

Section

Section 1

6. The simplest form

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')
LineWhat happensOutput
x = 5assignmentx -> 5
if x > 0:the condition is Truerun the body
print(...)indented, so it is the bodyx is positive
print('done')not indented, so it always runsdone

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

7. Picture it: the indentation decides what is conditional

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

The same rule as a function body and a loop body: indentation is structure.

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.

8. Worked example: a condition that is false

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')
LineWhat happensOutput
x = -3assignmentx -> -3
if x > 0:-3 is not greater than 0condition is False
print(...)skipped entirelyno output
print('done')not part of the bodydone

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

The whole run at once: each drop is one line of the program.

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.

9. Predict: how many lines does this print?

Prediction

Only one of the two print statements is conditional.

x = 0
if x > 0:
    print('positive')
    print('and that is that')
print('finished')
PartWhat happensOutput
x > 00 is not greater than 0False
both indented linesskipped togetherno output
print('finished')not indentedfinished

Predict first

How many lines of output?

  • Three
  • One
  • Two
  • None

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.

10. Worked example: pass, the body that does nothing

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!
LineWhat it doesEffect
if x < 0:the condition is evaluated as usualTrue or False
passa statement that does nothingno effect
the commenta note to the readerignored 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.

11. Trap: forgetting the colon

Trap

The 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.

The fix

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.

12. Sort: does this line run when the condition is false?

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?

runs anyway
print('b') — not indented; x = x + 1 — not indented; print('f') — not indented, after everything
skipped
print('a') — indented once; x = x + 1 — indented once; print('e') — indented twice
runs
None of these is indented under the if header, so none is part of its body. They run whatever the condition evaluated to.
skipped
Each is indented under the header, so each belongs to the body — including the doubly-indented one, which is inside something inside the body. A false condition skips the whole block.

13. Complete it: warn about a negative number

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.

14. Think it through: why must a body have at least one statement?

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.

15. Alternative execution: exactly one of two

Section

Section 2

16. if and else

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')
CaseThe conditionOutput
x = 44 % 2 == 0 is Truex is even
x = 77 % 2 == 0 is Falsex is odd
either wayexactly one branch runsalways 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

17. Picture it: the flow splits and rejoins

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.

18. Worked example: even or odd

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')
StepWhat happensResult
x % 2seven divided by two leaves one1
1 == 0the comparisonFalse
else branchthe condition was falsex 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

The whole run at once: each drop is one line of the program.

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.

19. Predict: which branch runs for zero?

Prediction

The boundary case is where most conditions go wrong.

x = 0
if x > 0:
    print('positive')
else:
    print('not positive')
StepWhyOutput
x > 00 is not greater than 0False
else branchruns whenever the condition is falsenot positive

Predict first

What does this print when x is 0?

  • positive
  • not positive
  • both lines
  • nothing

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.

20. Worked example: else is not a second condition

Worked example

A common misreading. Find out what else actually tests.

if x > 0:
    print('positive')
else:
    print('not positive')
CaseThe conditionWhich branch
x = 5condition Truepositive
x = -5condition Falsenot positive
x = 0condition Falsenot 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

Zero is on the right. else means the condition was false, not the opposite thing was true.

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.

21. Trap: two separate ifs where one if-else was meant

Trap

The 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.

The fix

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.

22. Discriminate: if-else or two separate ifs?

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?

if / else: one question, two outcomes
is the number even / is the number odd; did the file open / did it fail to open; is the list empty / does it have items
two separate ifs: two questions
is the user an adult / does the user have a ticket; is it raining / is it cold; is the word long / does the word start with a vowel
ifelse
Each pair is one question with two exhaustive, mutually exclusive answers. Writing them as separate ifs would work and would leave open the possibility of them drifting apart when edited.
two
Each pair is two independent questions. Both could be true, both could be false, and an else on the first would wrongly claim to cover the second.

23. Fill the middle: report whether a number is big

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.

24. Explain it yourself: why is exactly one branch guaranteed?

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.

25. Chained conditionals: elif, and the first-true rule

Section

Section 3

26. More than two possibilities

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')
BranchWhen it is checkedWhat happens
x < ychecked firstif true, print and stop
x > ychecked only if the first was falseif true, print and stop
elsereached only if both were falsethey 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

27. Picture it: conditions checked in order until one holds

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.

28. Worked example: a chain with three outcomes

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')
BranchThe testOutcome
x < y3 < 3False — check the next
x > y3 > 3False — fall through to else
elseno conditionx 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

The whole run at once: each drop is one line of the program.

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.

29. Predict: which branch runs?

Prediction

Two conditions are true. The order decides.

score = 95
if score > 50:
    print('pass')
elif score > 90:
    print('distinction')
ConditionValueWhat happens
score > 5095 is above 50True — this branch runs
score > 90would be True toonever checked
outputpassdistinction is unreachable

Predict first

What does this print for a score of 95?

  • distinction
  • pass
  • both lines
  • nothing

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.

30. Worked example: only the first true branch runs

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')
ConditionIts valueWhat happens
n % 2 == 012 is evenTrue — this branch runs
n % 3 == 012 is also divisible by 3never checked
outputone line onlydivisible 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

Same conditions, same value, different number of lines.

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.

31. Trap: ordering the branches wrongly

Trap

The 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.

The fix

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.

32. Rank: order these branches so all are reachable

Ranking

Most specific first, or the general condition swallows the specific one.

Put in order

  1. if n > 1000: print('huge')
  2. elif n > 100: print('large')
  3. elif n > 0: print('positive')
  4. else: print('zero or negative')

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.

33. Discriminate: elif or a separate if?

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?

a chain: exactly one applies
report a grade band: A, B, C; classify a number as negative, zero or positive; pick one of three menu options
separate ifs: any number may apply
warn about a long password AND warn about a missing digit; check whether to print a header AND whether to print a footer; apply a discount AND apply a delivery charge
chain
Each is a single classification with mutually exclusive outcomes. Exactly one grade band, one sign, one menu choice — so a chain expresses the intent and guarantees it.
sep
Each is several independent decisions that could all apply at once. Chaining them would mean a long password suppressed the missing-digit warning, which is not what anybody wants.

34. Find the counterexample: must a chain have an else?

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.

35. Nested conditionals, and why to avoid them

Section

Section 4

36. A conditional inside a conditional

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')
StructureWhat it containsCost
outer iftwo branchesequal, or everything else
inner ifinside the outer elseless than, or greater than
indentationthree levels deepharder 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

37. Picture it: the same three outcomes, two ways

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

Three outcomes either way. The left column stays flat as you add more.

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.

38. Worked example: flattening a nested conditional with and

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.')
VersionWhen the print runsDepth
nestedthe print runs only past both teststwo levels of indentation
flattenedthe print runs only if both are trueone level
equivalent?yes — and requires bothsame 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 whole run at once: each drop is one line of the program.

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.

39. Predict: are these equivalent?

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')
CaseWhat each version doesVerdict
a=1, b=1A prints 'both'; B prints 'both'agree
a=-1, b=1A prints 'a is not positive'; B prints nothingDISAGREE
a=1, b=-1A prints nothing; B prints nothingagree

Predict first

Are versions A and B equivalent?

  • Yes — the and condition captures both tests
  • No — B loses the else branch, so it stays silent when a is not positive
  • No — B is true more often than A
  • Yes, but B is slower

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.

40. Worked example: Python's chained comparison

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.')
FormWhen it appliesNote
0 < x and x < 10the general formworks for any two conditions
0 < x < 10the chained formworks only for comparisons on one value
equivalentfor this caseand 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

Both endpoints excluded, because both comparisons are strict.

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.

41. Trap: flattening a nested conditional that has an else

Trap

The 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.

The fix

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.

42. Complete it: flatten this nesting

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.

43. Compare: three ways to write a range test

Comparison

Fill the blanks. All three are correct; they differ in depth and readability.

Comparison matrix

FormHow it looksWhen to prefer it
nested ifstwo headers, two levelswhen the branches genuinely differ — otherwise never
andone header, both comparisons written outany two conditions, related or not
chained comparison0 < x < 10a 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.

44. Explain it: why avoid nesting?

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.

45. Putting it together: choosing the right shape

Section

Section 5

46. Four forms, and how to pick one

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

47. Picture it: how many branches can run?

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

One statement, or several statements. That is the structural difference.

Asking how many of these should be able to happen before writing anything is the fastest way to get the shape right.

48. Worked example: a menu, done correctly

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()
InputWhat is checkedWhat runs
choice = 'a'first condition truedraw_a, then done
choice = 'b'first false, second truedraw_b, then done
choice = 'z'all false, no elsenothing 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

The whole run at once: each drop is one line of the program.

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.

49. Predict: how many lines for a value that matches twice?

Prediction

The shape decides, not the conditions.

n = 12
if n % 2 == 0:
    print('even')
if n % 3 == 0:
    print('divisible by 3')
StatementWhat happensOutput
first if12 is eveneven
second ifa separate statement, tested independentlydivisible by 3
totaltwo independent tests, both truetwo lines

Predict first

How many lines does this print for n = 12?

  • One
  • Two
  • None
  • Three

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.

50. Worked example: independent checks, done correctly

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')
PasswordWhich conditions holdOutput
'abc'short, lowercase, all lettersthree warnings
'abcdefghij'long enough, lowercase, all letterstwo warnings
'Abc123def'long enough, has a capital, has digitsno 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.

51. Trap: the shape that works on your test data

Trap

The 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.

The fix

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.

52. Sort: which shape does each situation need?

Sorting

Ask how many branches should be able to run.

Sort into buckets

Sort each requirement by the conditional shape it needs.

if / elif / else
print a grade of A, B or C from a score; say whether a number is positive, negative or zero; choose one of four menu actions
separate if statements
print every validation error a form has; apply each discount the customer qualifies for
a single if, no else
warn only if the file is missing
chain
Each is one classification with mutually exclusive outcomes: one grade, one sign, one action. A chain guarantees exactly one branch and stops checking once it has an answer.
sep
Each is several independent conditions that may all apply. Chaining them would hide every problem after the first, which for validation errors and discounts is exactly wrong.
one
Only one case needs action and there is nothing to do otherwise. An else with pass in it would add a line and say nothing.

53. Reverse engineer: what shape produced this output?

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.

54. Where the shape matters most

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.

55. Compare: the four conditional shapes

Comparison

Fill the blanks. The right-hand column is the question that chooses between them.

Comparison matrix

ShapeHow many branches runUse it when
if alonezero or onethere is nothing to do in the other case
if / elseexactly oneone question with two exhaustive outcomes
if / elif / elseexactly oneone question with several mutually exclusive outcomes
separate ifsany numberthe 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.

56. The procedure: writing a conditional

Pattern

Six steps, and the shape is chosen before any code is written.

  1. List the cases you need to handle, in English.
  2. Ask how many of them should be able to happen for one input: exactly one, or any number.
  3. Exactly one means a chain; any number means separate if statements.
  4. Within a chain, put the most specific condition first, so that no branch is unreachable.
  5. Decide whether an else is needed — whether none of the above has anything to do.
  6. Test the boundary of every condition, and test one input that satisfies more than one.

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

57. Check yourself 1 of 3: the first-true rule

Check

Both conditions are true. The shape decides what happens.

n = 10
if n > 5:
    print('big')
elif n > 8:
    print('very big')
ConditionValueWhat happens
n > 510 is above 5True — this branch runs
n > 8would also be truenever checked
outputone linebig

Check your understanding

What does this print for n = 10?

  • A. big (correct)
  • B. very big
  • C. big and then very big
  • D. Nothing

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.

Why B tempts people
This would require the second condition to be checked first. Swapping the two branches would make it correct, which is exactly the fix for an unreachable branch.
Why C tempts people
A chain runs exactly one branch. Two lines would require two separate if statements, each tested independently.
Why D tempts people
The first condition is true, so a branch certainly runs. Nothing would print only if every condition were false and there were no else.

58. Check yourself 2 of 3: else

Check

One value tests whether you have read else correctly.

x = 0
if x > 0:
    print('positive')
else:
    print('negative')
StepWhat happensNote
x > 00 is not greater than 0False
elseruns whenever the condition is falsenegative
accuracyzero is not negativethe MESSAGE is wrong

Check your understanding

What is wrong with this program?

  • A. Nothing — it is correct
  • B. It prints 'negative' for zero, which is false (correct)
  • C. It is a syntax error, because else needs a condition
  • D. It prints nothing for zero

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.

Why A tempts people
The program runs without error, but it makes a false statement about zero. Correctness is about what it says, not only about whether it runs.
Why C tempts people
else takes no condition by design. Giving it one is the syntax error, not omitting one.
Why D tempts people
Exactly one branch of an if-else always runs, so something is always printed. Zero takes the else branch.

59. Check yourself 3 of 3: flattening

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?

  • A. if a > 0 or b > 0:
  • B. if a > 0 and b > 0: (correct)
  • C. if not (a > 0 and b > 0):
  • D. if 0 < a < b:

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.

Why A tempts people
or is true when either condition holds, so it would print when a is positive and b is not — a case the nested version skips.
Why C tempts people
not reverses the whole thing, so this prints in exactly the cases the original does not.
Why D tempts people
This is a chained comparison meaning a is above zero and below b, which says something about the relationship between a and b that the original never tested.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

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?

  • Both print two lines
  • The chain prints one, the separate ifs print two
  • Both print one line
  • The chain prints two, the separate ifs print one

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • The basic if: header, colon, indented body, and what the body includes
  • if / else, and the fact that exactly one branch always runs
  • Chains: the first-true rule and getting the branch order right
  • Flattening nested conditionals with and, or with a chained comparison

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Two pages, and your programs can finally do different things on different days.

If you remember one thingIt is this
From the basic ifThe indentation decides what is conditional. A body needs at least one statement; pass is one.
From elseIt has no condition. It covers everything the if excluded, including the boundary case.
From chainsOnly the first true branch runs, even if several conditions are true.
From the shapesExactly one branch means a chain; any number means separate ifs.
From nestingFlatten 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

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §5.4-5.7, pp. 41-42
  2. Python documentation — More Control Flow Tools
  3. Python documentation — Expressions

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108