Skip to content
  • Home
  • How To
  • M0
  • Module 1 - Python
  • Agents
  • M2
  • M3
  • M4
  • M5
  • M6
  • Progress
  • Contact
  • Pricing
  • Sign in
  • Future
  1. Module 1M1
  2. ›
    S2Control Flow
  3. ›
    Lesson 1 of 4Conditionals - if, elif, else, and Truthiness
  4. ›
    S2›Lesson 1 of 4
  1. Module 1
  2. ›
  3. Section 2
  4. ›
  5. Lesson 1

Conditionals - if, elif, else, and Truthiness

FREE·~1.5 h·2 labs·4 questions·1 assignment


Context

A conditional runs one block of code instead of another based on whether a question is true. Python marks which lines belong to each block by indentation, so the shape of the text on the page is the structure of the program, not decoration.

Branching is what makes a script react to its data rather than assume it. A row with a missing price gets skipped while a complete row gets kept, a metric above the previous best triggers saving the model while one below it does not, and a value outside a plausible range gets repaired instead of trusted. Every data cleaning step you write is a decision made once per row.

Truthiness is where this quietly goes wrong. Python treats an empty piece of text, a zero, and an empty collection as false, so a check meant to ask whether a field is present will also reject a legitimate measurement of zero, and a whole category of valid rows disappears with no error raised. The previous lesson produced true and false answers by comparing values, this lesson is where those answers finally change what the program does.


Goal

  • Write a multi-branch decision with if, elif, and else and predict which single branch runs for any given input.
  • Group statements into a block using correct, consistent indentation.
  • State the truthiness of common values, 0, 0.0, "", None, non-empty strings, without converting them explicitly.
  • Build compound conditions with and, or, and not to gate a branch.
  • Write a range check the math way using comparison chaining, e.g. 0.0 <= x <= 1.0.

Prerequisites

Neither of these is required and the lesson assumes you opened neither. They are here only if you would rather have the picture before the detail, and Theory builds both from nothing.

  • YouTube - "Python Tutorial for Beginners 6: Conditionals and Booleans - If, Else, and Elif Statements" by Corey Schafer. The segment on if/elif/else. Watch for one picture, execution taking exactly one fork in the road.
  • Official docs - Built-in Types, "Truth Value Testing". Short, and it is the authoritative rule for which values count as false, with the common ones listed.

Table of Contents

  • Conditional Statement (if / elif / else)
  • Code Block and Indentation
  • Branch
  • Truthiness
  • Comparison Chaining

Theory

Conditional Statement (if / elif / else)

What. A conditional statement runs a block of code only when a condition is true. if tests the first condition, elif ("else if") tests further conditions in turn, and else catches everything that matched none of them. Why. A program with no conditionals can only do one fixed thing. Conditionals are what let it react, take this action for valid data, that action for missing data, and they are the first step from a calculator toward an algorithm. When. Use a conditional whenever the next action depends on a value: gating a computation, validating an input, or choosing a label.

Nothing needs installing, if, elif, and else are keywords built into the language. The shape is fixed: the keyword, a condition, which Python judges as true or false, a colon :, then an indented block that runs when the condition is true, and else alone takes no condition. When the block is one statement that opens no block of its own, a single print for example, it may instead sit on the same line, after the colon, as in if score > 0.9: print("pass"), and the same holds for the loops and functions to come.

accuracy = 0.93
# 'if' tests a condition, the indented block runs ONLY when it is True.
if accuracy >= 0.9:
    print("model accepted")  # runs because 0.93 >= 0.9 is True

The colon ends the if line and the indentation marks what belongs to the branch. Had the condition been False, the block would be skipped entirely and nothing at all would print, which is worth noticing now: a conditional that matches nothing is silent, not broken.

To handle more than one case, chain elif clauses and finish with an optional else. Python checks the conditions top to bottom and runs the block of the first one that is true, then skips all the rest.

loss = 0.4
# Checked top to bottom, the FIRST true condition wins, the others are skipped.
if loss < 0.1:
    print("excellent")
elif loss < 0.3:
    print("good")
elif loss < 0.5:
    print("acceptable")  # 0.4 < 0.5 is the first True, so this runs
else:
    print("poor")

Python tests loss < 0.1, then loss < 0.3, then loss < 0.5, and stops at the first true one, so the else never runs. The else runs only when every condition above it was false. Exactly one block runs, never more.

DON'T:

  • DON'T forget the colon : that ends each if/elif/else header, leaving it off is a syntax error.
  • DON'T expect more than one branch to run, once a condition matches, Python skips the rest of the chain.

Code Block and Indentation

What. A code block is a group of statements that belong together and run as a unit. In Python, a block written on the lines below its colon is marked by indentation, leading whitespace, not by braces or keywords. Why. Indentation is syntax in Python, not decoration. The whitespace literally tells the interpreter which lines are inside the if and which come after it. Get it wrong and the program either errors or runs the wrong lines. When. Every time a line ends in a colon, if, elif, else, and soon loops and functions, the lines that follow must be indented to form that construct's block.

The standard is four spaces per indentation level. Every line in the same block must be indented by the same amount. The block ends when the indentation returns to the previous level.

n_rows = 100
if n_rows > 0:
    # These two indented lines are BOTH inside the if-block.
    print("dataset has rows")
    print("proceeding to clean")
# This line is NOT indented, so it runs regardless of the condition.
print("done checking")

For a block on the lines below the if, the indentation is the only thing distinguishing "inside the if" from "after the if". There is no closing keyword and no brace, so the left margin is the entire signal.

Mixing indentation is a real error, not a style nitpick. Inconsistent spacing inside one block raises an IndentationError, mixing tabs and spaces inside one raises either that or TabError, a kind of IndentationError, depending on the mix, and in the file you run either one stops the program before it runs.

score = 0.8
if score > 0.5:
    print("pass")
    # The next line has the WRONG indentation and would raise IndentationError:
    #   print("extra")   # <- only 2 spaces while the block uses 4

Configure your editor to insert four spaces when you press Tab, which VS Code does automatically once the Python extension from Module 0's setup page is installed, and this class of error stops happening rather than being caught.

DON'T:

  • DON'T mix tabs and spaces for indentation, pick spaces (four of them) and never mix, since mixing them inside one block raises an IndentationError or its kind TabError, depending on the mix.
  • DON'T change the indentation amount within one block, every line in the same block must align exactly.

Branch

What. A branch is one of the mutually exclusive paths execution can take through a conditional, the if block is one branch, each elif block another, the else block the last. Why. Thinking in branches is how you reason about a conditional's behavior: for any given input, at most one branch executes, exactly one when the chain ends in else, and naming them makes it easy to check you have covered every case. When. Reason about branches whenever you write or debug a conditional, ask "which branch runs for this input, and is there an input that falls through every branch?"

The defining property is mutual exclusion: in an if/elif/else chain, the paths are alternatives, and execution takes at most one of them. This is different from writing several separate if statements, where more than one can run.

temperature = 75
# ONE chain -> exactly ONE branch runs (the first match).
if temperature > 90:
    label = "hot"
elif temperature > 60:
    label = "warm"  # this branch is taken, 75 > 60 is the first match
else:
    label = "cold"
print(label)        # warm

The three branches are mutually exclusive paths and the input picked exactly one. That is the habit worth forming: trace any conditional by asking which single branch a given value lands in, and then whether some other value lands in none of them.

Contrast this with separate if statements, which are not mutually exclusive, each is evaluated independently, so several can fire.

x = 5
# TWO independent ifs -> more than one block can run.
if x > 0:
    print("positive")  # runs
if x > 3:
    print("big")       # ALSO runs

Both lines execute, because these are two unrelated statements rather than one chain. Use a single if/elif/else when the cases are alternatives and you want exactly one, and separate ifs only when the checks are genuinely independent questions.

DON'T:

  • DON'T use separate if statements when you mean mutually exclusive cases, chain them with elif, or multiple blocks may run.
  • DON'T leave a gap in your branches by accident, if no else is present and nothing matches, no branch runs at all.

Truthiness

What. Truthiness is how Python treats a non-boolean value when a yes/no test needs a boolean. Every value of Python's built-in types is either "truthy" (counts as True) or "falsy" (counts as False) in a condition. Why. It lets you write if name: instead of if name != "":, which is shorter and idiomatic. But it defies the beginner intuition that only True and False belong in a condition, so it must be learned deliberately. When. It applies any time a non-boolean value sits where a condition is expected, checking whether a string is non-empty, a number is non-zero, or a value is present rather than None.

The falsy values are a short, memorizable list: False, None, the number zero in any type (0, 0.0), and empty text (""). Empty collections like an empty list are also falsy, which you will meet in a later section on collections. Everything else is truthy, any non-zero number, any non-empty string.

# These all count as FALSE in a condition (falsy values):
print(bool(0))       # False
print(bool(0.0))     # False
print(bool(""))      # False
print(bool(None))    # False
# These all count as TRUE in a condition (truthy values):
print(bool(42))      # True (any non-zero number)
print(bool("loss"))  # True (any non-empty string)

bool(value) makes the truthiness explicit, but you rarely write it, because Python applies it automatically to whatever sits in a condition.

In practice this lets conditions read naturally. Recall from Lesson 1.1.2 that None is the "no value yet" placeholder, its falsiness means if value: cleanly skips both "absent" and "empty" cases at once.

model_name = ""
# No need for 'model_name != ""', an empty string is falsy on its own.
if model_name:
    print("name:", model_name)
else:
    print("no name set")   # runs, because "" is falsy

best_score = None
# 'None' is falsy, so this is a quick "has a value been set?" check.
if best_score:
    print("score:", best_score)
else:
    print("no score yet")  # runs, because None is falsy

This is the everyday use of truthiness, short readable checks for "is this empty or missing?" without spelling out a comparison.

Now the caution that the rest of this lesson keeps returning to. if best_score: treats a real score of 0.0 as falsy too, because truthiness cannot tell "nothing was set" apart from "something was set and it was zero". When zero is a legitimate value, use the explicit is None from Lesson 1.1.2 instead, which asks about absence and nothing else.

DON'T:

  • DON'T assume only True/False are allowed in a condition, any built-in value works, judged by its truthiness.
  • DON'T use if value: when a real 0 or 0.0 is meaningful, it would be treated as falsy, use if value is not None: instead.

Comparison Chaining

What. Comparison chaining lets you write a double-sided comparison the way mathematics does, 0.0 <= x <= 1.0, instead of joining two separate comparisons with and. Why. It reads exactly like the math inequality it represents, so range checks become clear and short. It is also less error-prone than repeating the variable in an and expression. When. Use it whenever you need to test that a value falls between two bounds, a probability in [0, 1], an index within a valid range, a measurement within tolerance.

A chained comparison like a <= x <= b is shorthand that Python expands to a <= x and x <= b, evaluating x only once. Any mix of comparison operators can be chained, though range checks are by far the most common use.

accuracy = 0.87
# Reads like math: is accuracy between 0.0 and 1.0 inclusive?
if 0.0 <= accuracy <= 1.0:
    print("valid probability")  # runs, 0.87 is in range
# Equivalent long form using 'and' from Lesson 1.1.4:
if 0.0 <= accuracy and accuracy <= 1.0:
    print("valid probability (long form)")

Both conditionals do the same thing and both print. The chained form is shorter, mirrors the inequality directly, and names accuracy once instead of twice, which is the part that actually prevents bugs: the long form lets you mistype the second accuracy as something else and still run.

Chaining combines naturally with the logical operators when a condition has several parts. Comparisons outrank and and or (recall precedence from Lesson 1.1.4), so and/or join whole comparisons.

x = 5
# Chain inside a larger compound condition.
if 0 <= x <= 10 and x != 7:
    print("in range and not seven")  # runs, 5 is in [0,10] and not 7

Chaining handles the range portion while and adds the exclusion, which is the common shape for validating one value against several constraints at once. Comparisons outrank and, so no parentheses are needed.

DON'T:

  • DON'T write 0 < x and < 10, the chained form is 0 < x < 10, each side of a bare and still needs a complete expression.
  • DON'T read a < x > b as a range check, a chain only compares neighbors, so it means a < x and x > b and says nothing about a against b.

Lab Exercises

Both problems first, then both solutions. Do not peek.

Lab Exercise 1: The Grade Gate, and What Belongs to It

Covers the first half (Conditional Statement, Code Block and Indentation, Branch).

In a script grade.py, bind score = 0.82. Write an if/elif/else chain that prints "A" if score is at least 0.9, "B" if at least 0.8, "C" if at least 0.7, and "F" otherwise. Run it and confirm it prints B. Then change score to 0.95 and to 0.5 and confirm the branch that runs changes each time. Explain in a comment why only one letter ever prints.

Then, in the same file, bind n_rows = 0 and write an if n_rows > 0: whose block holds two indented print lines, followed by one un-indented print("check complete"). Run it again and observe that, of the three new print lines, only check complete appears. In a final comment, say which lines are inside the block and which are outside, and why the un-indented line ran while the other two did not.

Lab Exercise 2: Truthiness, and the Value That Passes One Test and Fails the Other

Covers the second half (Truthiness, Comparison Chaining).

Create validate.py. For each of these values use an if value:/else to print either "truthy" or "falsy": 0, 42, "", "data", None, 0.0, and "0". Confirm which four are falsy, and note in a comment why "0" is not one of them.

Then simulate cleaning one row. Bind age = 34, name = "goku" and score = 0.91, and write a single compound conditional that accepts the row only if age is chained-between 0 and 120, name is truthy, and score is chained-between 0.0 and 1.0. Print "row kept" or "row dropped".

Finally, rebind age = 0 and print both 0 <= age <= 120 and bool(age) on their own lines. In a comment, explain how one value can be reported as in range and as falsy at the same time, and which of the two questions a row validator actually means to ask about an age.


Solution 1: The Grade Gate, and What Belongs to It
score = 0.82
# Checked top to bottom, the FIRST true condition wins and the rest are skipped.
if score >= 0.9:
    print("A")
elif score >= 0.8:
    print("B")  # 0.82 >= 0.8 is the first True, so this runs
elif score >= 0.7:
    print("C")
else:
    print("F")
# Only one letter prints because an if/elif/else chain runs exactly ONE branch,
# once a condition matches, Python skips every remaining condition.

n_rows = 0
if n_rows > 0:
    # Both indented lines are INSIDE the block, they run only if the condition holds.
    print("dataset has rows")
    print("proceeding")
# Back at the left margin, so OUTSIDE the block, and it always runs.
print("check complete")

Run it:

# Execute the grade gate and the indentation demo.
python3 grade.py

The chain prints B. Changing score to 0.95 makes the first condition true so it prints A, and 0.5 falls through every condition to print F. Only one letter ever appears because the branches are alternatives and execution takes exactly one.

The second half prints only check complete. The two indented lines are the if block and are skipped, because 0 > 0 is false. What makes them the block is nothing but their indentation, and what puts the last line outside it is nothing but the left margin.

DON'T:

  • DON'T reorder the conditions to put >= 0.7 first, a 0.95 score would then wrongly print C, since the first match wins.
  • DON'T use separate if statements for the grades, that could print several letters for one score.
  • DON'T indent print("check complete") by accident, that would pull it into the block and it would stop printing.
Solution 2: Truthiness, and the Value That Passes One Test and Fails the Other
# Each value tested on its own, no loop needed yet.
if 0:
    print("0 -> truthy")
else:
    print("0 -> falsy")        # zero is falsy
if 42:
    print("42 -> truthy")      # any non-zero number is truthy
else:
    print("42 -> falsy")
if "":
    print("'' -> truthy")
else:
    print("'' -> falsy")       # an empty string is falsy
if "data":
    print("'data' -> truthy")  # any non-empty string is truthy
else:
    print("'data' -> falsy")
if None:
    print("None -> truthy")
else:
    print("None -> falsy")     # None is falsy
if 0.0:
    print("0.0 -> truthy")
else:
    print("0.0 -> falsy")      # zero-float is falsy
if "0":
    print("'0' -> truthy")     # NON-EMPTY string, so truthy despite the digit
else:
    print("'0' -> falsy")

age = 34
name = "goku"
score = 0.91

# Two chained range checks plus a truthiness check, joined by 'and'.
if 0 <= age <= 120 and name and 0.0 <= score <= 1.0:
    print("row kept")
else:
    print("row dropped")

# The same value answering two different questions.
age = 0
print(0 <= age <= 120)         # True
print(bool(age))               # False
# 'in range' and 'truthy' are not the same question. The range check asks whether
# the number is a plausible age, and 0 is. Truthiness asks whether the value is
# non-zero, and 0 is not. A validator means the first one, so an age of 0 must be
# range-checked, never truth-checked, or every zero is silently thrown away.

Run it:

# Print each truthiness verdict, validate the row, then contrast the two questions.
python3 validate.py

The four falsy values are 0, "", None and 0.0. "0" is truthy, because the quotes make it a non-empty string and truthiness looks at emptiness rather than at what the characters happen to spell.

The last three lines are the ones to sit with. One value, 0, is reported in range and falsy at the same time, and neither report is wrong. They are answers to different questions, and picking the wrong question is how a legitimate zero disappears from a dataset without any error being raised.

DON'T:

  • DON'T use if value: when a real 0 or 0.0 is meaningful, it is falsy, so range-check it or use is not None.
  • DON'T assume "0" is falsy, it is a non-empty string and therefore truthy, despite looking like zero.
  • DON'T expand the chained 0 <= age <= 120 into 0 <= age and age <= 120 out of habit, the chained form names the value once and cannot disagree with itself.

Theory Assessment

Conceptual reinforcement, not a coding section. All four questions with their hints first, then all four answers.

Q1: One Branch, and Which One

Two parts on how a chain executes. First, explain why an if/elif/else chain runs exactly one branch, and how that differs from writing three separate if statements. Second, for loss = 0.25, state the output of the chain below and say exactly which conditions were checked and which were not:

if loss < 0.1:
    print("excellent")
elif loss < 0.3:
    print("good")
elif loss < 0.5:
    print("acceptable")
else:
    print("poor")

Hint: One structure offers mutually exclusive paths, the other offers independent tests. For the trace, conditions are tested top to bottom and stop at the first true one, so "not checked" is a real answer.

Q2: The Colon and the Left Margin

A student writes if score > 0.5 with no colon and gets a SyntaxError. Explain what the colon does and where the block goes. Then say what would have happened instead if they had written the colon correctly but left the next line un-indented, and why Python treats that as a different kind of mistake.

Hint: The colon opens a block and the indentation is what fills it. Ask what Python has if it is promised a block and then handed nothing at that deeper level.

Q3: Falsy, Truthy, and Why if name: Reads Well

Two parts. First, state whether each of these is truthy or falsy in a condition: 0, "0", "", None, 3.14, 0.0. Second, explain why if name: is a valid way to check that a string is non-empty, and what it does when name is "".

Hint: Zero, empty and None are falsy. Note carefully the difference between the number 0 and the string "0", which is the one most people get wrong.

Q4: The Reading That Was Really Zero

A sensor pipeline validates each reading with if measurement: before recording it. Readings of 0.0 are being dropped, silently, with no error and nothing in the logs. Separately, the same pipeline validates ages with 0 <= age <= 120, and an age of 0 passes that check without complaint.

Answer three things. First, explain the dropped readings, and why no error was ever raised. Second, the value 0 is falsy AND passes a range check that includes it, in the same program, say what question each of the two tests is actually asking, and why both answers are correct. Third, a colleague proposes fixing the sensor check with if measurement != 0:. Explain why that does not fix it, name the check that does, and say what it asks that neither of the other two does.

Hint: Three different questions are in play, is this non-zero, is this within bounds, and does this exist at all. Work out which of the three each candidate test answers before deciding which one the pipeline meant.


Answer 1:

An if/elif/else chain is a single structure offering mutually exclusive paths. Python evaluates the conditions top to bottom, runs the block of the first one that is true, and then skips the entire rest of the chain, so exactly one branch executes. Three separate if statements are three independent questions, each evaluated on its own regardless of what the others did, so zero, one, two or all three of their blocks may run.

For loss = 0.25, the output is good. Python checked loss < 0.1, which was false, then checked loss < 0.3, which was true, and ran that branch. loss < 0.5 was never checked at all and the else never ran, and that is not a technicality: once a condition matches, the remaining conditions are not evaluated, so anything expensive or fragile sitting in a later condition is never reached.

DON'T use separate if statements when you mean mutually exclusive cases, chain them with elif, or multiple blocks may run.

Answer 2:

The colon ends the header and announces that a block follows. It is what turns if score > 0.5 from an incomplete sentence into the opening of a compound statement, and what belongs to that branch is what follows the colon, the lines indented beneath it or, in the one-line form, the rest of that line. Without the colon Python reaches the end of the line still expecting one, and reports a SyntaxError.

Writing the colon and then leaving the next line un-indented is a different failure, and Python names it differently: it raises an IndentationError saying an indented block was expected. The distinction is worth keeping, because the two errors point at different halves of the same construct. The SyntaxError means the block was never opened, and the IndentationError means the block was opened and then never filled.

DON'T treat indentation as a matter of style here, it is the syntax that decides which lines belong to the branch, and the interpreter enforces it.

Answer 3:

0 is falsy, "0" is truthy, "" is falsy, None is falsy, 3.14 is truthy, and 0.0 is falsy. "0" is the trap: the quotes make it a string, truthiness asks whether a string is empty rather than what its characters spell, and a one-character string is not empty.

if name: works because a condition does not require a boolean. Python judges any built-in value it is given by truthiness, and for a string that means non-empty is true and empty is false, so if name: is a shorter and idiomatic way of writing if name != "":. When name is "" the condition is false and the block is skipped entirely.

DON'T assume only True and False are allowed in a condition, any built-in value works and is judged by its truthiness.

Answer 4:

Nothing errored because nothing went wrong, mechanically. 0.0 is falsy, so if measurement: evaluated to false and the step that records the reading was skipped, which is exactly what the code says to do. A skipped branch is silent by design, so a reading that never got recorded looks identical to a reading that never arrived. This is the failure mode worth fearing, a wrong answer produced by correct execution.

The two tests disagree because they ask different questions, and both answer their own correctly. 0 <= age <= 120 asks is this value within these bounds, and 0 is, because an age of zero is a plausible age. if measurement: asks is this value non-zero and non-empty, and 0.0 is not. Neither is buggy on its own. The bug is that the pipeline used the second question where it meant a third one.

if measurement != 0: does not fix anything, it asks nearly the same question out loud. It still rejects a real 0.0, for the same reason and with the same silence, and it now lets a missing None through. The check that works is if measurement is not None:, and what it asks is whether a value exists at all, which is the only one of the three questions that can separate "no reading arrived" from "a reading arrived and it was zero". Absence and zero are different facts, and only a test about absence can tell them apart.

DON'T reach for truthiness on any value whose valid range includes zero. And DON'T treat the absence of an error as evidence of correctness, this entire bug ran cleanly every time.


Assignment

One assignment. The problem and its hints first, then the solution.

Assignment: The Threshold Router

Create router.py, simulating a model-serving decision. Bind confidence = 0.0, is_flagged = False and region = "EU".

Route the request with a single chain. If confidence is None or is not chained-between 0.0 and 1.0, print "reject: bad confidence". Otherwise, if the request is_flagged, print "route: manual review". Otherwise, if confidence >= 0.8 and region is truthy, print "route: auto-approve". Otherwise print "route: default queue".

Then, as a single line, compute tier = "high" if confidence >= 0.8 else "low" using a conditional expression, and print it.

In comments, settle three things: trace which branch runs for the given inputs and why, explain why a real confidence = 0.0 has to be validated with the range check and is None rather than a bare if confidence:, and state what would break if the is_flagged branch were moved above the bad-confidence guard.

Hints:

  • Order matters. The guard has to come first so that every branch below it can assume confidence is a valid number already in range, which is the only reason those branches can compare it without re-checking.
  • The one-line form is VALUE_IF_TRUE if CONDITION else VALUE_IF_FALSE, and it is an expression producing a value, not a statement, which is why its result can be bound to a name.

Solution:
confidence = 0.0
is_flagged = False
region = "EU"

# Guard FIRST: reject absent or out-of-range confidence before trusting it below.
if confidence is None or not (0.0 <= confidence <= 1.0):
    print("reject: bad confidence")
elif is_flagged:
    print("route: manual review")
elif confidence >= 0.8 and region:
    print("route: auto-approve")
else:
    print("route: default queue")  # this is the branch that runs

# One-line tier using a conditional expression. It sits outside the chain, so
# the guard does not protect it: a None confidence raises TypeError here.
tier = "high" if confidence >= 0.8 else "low"
print(tier)                        # low

# Trace: confidence is 0.0, which is not None, and 0.0 <= 0.0 <= 1.0 is True, so
# the guard does NOT fire. is_flagged is False, so that branch is skipped.
# confidence >= 0.8 is False, so that one is skipped too, and the else runs.
# A bare 'if confidence:' would have treated the real 0.0 as falsy and rejected a
# perfectly valid reading, so validity is judged by the range check and 'is None',
# which ask about bounds and about absence rather than about being non-zero.
# Moving the is_flagged branch above the guard would let a flagged request with a
# confidence of None or 99.0 reach manual review as though it were well-formed,
# because the chain stops at the first match and the guard would never be reached.

Run it:

# Route the simulated request.
python3 router.py

The router prints route: default queue, then low.

The ordering question in the last comment is the one worth keeping. The confidence >= 0.8 branch after the guard compares confidence against a number, and that comparison is only meaningful because the guard has already established that confidence is a real number inside [0.0, 1.0]. Move the guard down and the branches it passes start running against values it was supposed to catch. Because a chain stops at the first match, the position of a guard is not a stylistic preference, it is the thing that makes every branch below it in the chain safe to write.

DON'T put the is_flagged or confidence >= 0.8 branch before the bad-confidence guard, every branch below a guard is written assuming the guard already ran. And DON'T reach for if confidence: as a shorthand for validity, a legitimate 0.0 is falsy and would be rejected without a word.


Additional Resources

Optional, and nothing later depends on either one. Worth opening only if this lesson left a question behind.

  • YouTube - "Python Tutorial for Beginners" by freeCodeCamp.org. The segment on if statements and indentation, for a second visual pass over branching and blocks.
  • Official docs - The Python Tutorial, "match Statements". A branching form that dispatches on the SHAPE of a value rather than on a condition, worth meeting once elif chains feel natural.

Summary

This lesson is where a program stops being a fixed recipe and starts making choices. The if/elif/else chain is a fork in the road: execution arrives, evaluates the conditions in order, takes at most one path, and never looks back at the others. Reasoning about code becomes a habit of asking, for any input, which single branch it lands in, and whether some input might quietly fall through every branch you wrote. That branch-by-branch thinking is the core skill, and it carries straight into loops, functions, and every structured program you will ever read.

Two ideas in this lesson fight the intuitions beginners arrive with, and both are worth the friction. The first is that indentation is not decoration but syntax, the whitespace itself is what tells Python which lines belong to a branch, so sloppy spacing is a real bug, not a style preference. The second is truthiness: the discovery that conditions do not demand a literal True or False but will judge any built-in value, with zero, empty, and None counting as false and everything real counting as true. Comparison chaining rounds it out by letting range checks read like the mathematics they encode, and the conditional expression gives you a compact one-line choice when a full block would be overkill.

The payoff is immediate and practical, even though no machine learning has appeared yet. Deciding whether a data row is clean enough to keep, is the age plausible, is the label present, is the probability in range, is nothing more than a conditional, and you will write hundreds of them when you start cleaning real tables. Every filter, every validation rule, every "handle this case differently" is an if in disguise. A program can now choose, and the next lesson makes it repeat.


Cheat Sheet

Part A - Reference Table

Concept Syntax Behavior When to avoid
if/elif/else if cond: ... elif cond: ... else: Runs the first true branch only Separate ifs when cases are exclusive
Block / indentation 4 spaces under a : line Groups statements into a unit Mixing tabs and spaces
Branch each if/elif/else body Mutually exclusive paths Assuming several run at once
Truthiness if value: Falsy: 0 0.0 "" None False, empty collections, rest truthy When a real 0/0.0 is valid
Comparison chaining 0 <= x <= 1 Same as 0<=x and x<=1 Non-range chains like a < x > b
Conditional expr a if cond else b One-line value choice Multi-statement branches

Part B - Compact Code Block

if score >= 0.9:                      # colon ends the line
    grade = "A"                       # 4-space indented block
elif score >= 0.8:                    # checked only if above was False
    grade = "B"
else:                                 # runs if nothing matched
    grade = "C"

if name:                              # truthiness: "" / None / 0 are falsy
    print(name)
if best is not None:                  # not 'if best:', which skips a valid 0.0
    print(best)

if 0.0 <= p <= 1.0:                   # chained range check, reads like math
    print("valid")

tier = "high" if p >= 0.8 else "low"  # one-line conditional value
←Previous: Section 1, Lesson 4
Next: Lesson 2→

Questions about this lesson: #module-1 on Discord

Found an error in this lesson? Report it

Your progress
Current streak
🔥 0
Lessons marked today
0 / 3
Longest streak
0

Sign in

The bootcamp

  • Curriculum
  • How To
  • Pricing
  • Changelog
  • Future

The community

  • Discord
  • Reviews
  • Contact

Follow

  • Instagram
  • X
  • TikTok
  • Reddit

Legal

  • Terms
  • Privacy
  • Refunds

Coming soon on November 16, 2026

Theme