Python conditionals — if, elif and else theory with FP1 grades, a ladder of decisions (and the 9.2 that came out “Pass”)

In my first FP1 assignment I had to write a grade classifier. I tried it with a 6 and it said “Pass”. I tried it with a 3 and it said “Fail”. Perfect, I handed it in. The next day a classmate tried it with the 9.2 he got in the midterm, and the program told him, very seriously, that he had passed. Not outstanding, not very good: pass. The code was this:

# The classifier from my first assignment (with the bug)
grade = 9.2

if grade >= 5:                  # with 9.2 this one is already True...
    grade_name = "Pass"
elif grade >= 7:                # ...so Python does not even look at this one
    grade_name = "Very good"
elif grade >= 9:                # nor this one
    grade_name = "Outstanding"
else:
    grade_name = "Fail"

print(f"Grade {grade} → {grade_name}")

And this was the output, without a single error message:

Grade 9.2 → Pass

Python didn’t get it wrong: it did exactly what I asked. I first asked “is it greater than or equal to 5?”, a 9.2 said yes, and that was the end of the conversation. The other two questions were never even looked at. By the end of this article you’ll know why, and it won’t happen to you again.

Today’s topic turns a program that calculates into a program that decides: Python conditionals, with if, elif and else. Until now your programs ran every line, top to bottom, skipping none. With a conditional, Python runs some lines or others depending on whether a condition is met. If you’re coming from Python operators, half the theory is already done: comparisons (>=, ==, !=) and logical operators (and, or, not) are exactly what goes inside each condition. After this article you have the conditionals practice with three complete programs and the exercises with solutions.

What is a conditional?

A conditional is an instruction that tells Python: “if this is true, do this; if not, do something else (or nothing)”. We do it all the time without thinking: if it’s raining, I take the umbrella; if not, I don’t. In Python it’s written almost the same way:

raining = True

if raining:                     # raining is already a boolean: no need for == True
    print("Taking the umbrella")
else:                           # the else has no condition
    print("I don't need an umbrella")

Since raining is True, the program prints:

Taking the umbrella

The condition is any expression that gives True or False. Here it’s a boolean variable, but usually it’s a comparison like grade >= 5, which Python evaluates on the spot: with grade = 7.5 it gives True, and with grade = 4.0 it gives False. Notice there’s no need to write if raining == True:. raining is already True or False, and comparing it with True just adds noise.

In the FP1 notes, conditionals appear as “conditional execution”: the if statement decides whether a block runs or is skipped, and in both cases the program carries on with the instruction below the block.

The if statement

The simplest form is a lone if. If the condition is True, it runs its block; if it’s False, it skips the whole block:

grade = 7.5

if grade >= 5:                      # the condition is True or False
    print("Pass")                   # 4 spaces: this line belongs to the if
    print("On to the next one!")    # and this one too
print("End of program")             # no indentation: always runs

With grade = 7.5 all three lines come out:

Pass
On to the next one!
End of program

Now change only the data to grade = 4.0 (same program, different value). The output is:

End of program

The two indented lines disappear and “End of program” still shows up. Why? Because Python knows which lines belong to the if by their indentation, not by braces or a closing keyword. Three non-negotiable rules:

  • The colon : at the end of the if line is mandatory. Without it, SyntaxError.
  • Indentation (4 spaces) marks the block. Every line below the if with that indentation belongs to it.
  • The first line without indentation closes the block and always runs.
LinePart of the if?With 7.5With 4.0
print("Pass")Yes (4 spaces)RunsSkipped
print("On to the next one!")Yes (4 spaces)RunsSkipped
print("End of program")No (0 spaces)RunsRuns

Set VS Code so the Tab key inserts 4 spaces (that’s the default for .py files). Mixing tabs and spaces in the same block is one of the silliest ways to earn an IndentationError or a TabError.

The if/else structure

When you want to do one thing if the condition is true and something else if it isn’t, you add else:

grade = 4.0

if grade >= 5:
    print("Pass")
else:                                            # only if grade >= 5 was False
    print("Fail")
    print(f"You are {5 - grade} points short")   # 5 - 4.0 = 1.0
print("End of program")

With a 4.0 it goes through the else:

Fail
You are 1.0 points short
End of program

Check by hand: 5 - grade is 5 - 4.0 = 1.0. It prints 1.0 and not 1 because grade is a float, and subtracting with a float gives a float (you saw this with operators).

The else has no condition: it runs exactly when the if condition was False. That’s why an if/else always runs one and only one of its two blocks. Never both, never neither.

Giving the else a condition (else grade < 5:). It’s a SyntaxError. If you need another condition, that’s an elif.

The if/elif/else structure

Two paths only need if/else. But a grade isn’t just “pass” or “fail”: there’s outstanding, very good, pass and fail. To choose between more than two paths you use elif, short for else if:

grade = 7.5

if grade >= 9:              # 1st question: 7.5 >= 9? False, moves on
    grade_name = "Outstanding"
elif grade >= 7:            # 2nd question: 7.5 >= 7? True, goes in and finishes
    grade_name = "Very good"
elif grade >= 5:            # no longer evaluated
    grade_name = "Pass"
else:                       # only if all the previous ones were False
    grade_name = "Fail"

print(f"{grade} → {grade_name}")

Output:

7.5 → Very good

This is what Python does with grade = 7.5, step by step:

StepQuestionResultWhat happens
1grade >= 9 → 7.5 >= 9FalseMoves down to the next elif
2grade >= 7 → 7.5 >= 7TrueRuns its block: grade_name = "Very good"
3grade >= 5Not evaluatedJumps to the end of the structure
4elseNot evaluated—

The diagram sums it up: Python walks down the ladder asking questions, and as soon as one answer is True it goes through that door and leaves the structure.

How an if/elif/else chain runsWith grade 7.5, Python checks grade >= 9 (False), then grade >= 7 (True) and assigns Very good; grade >= 5 and else are not evaluated.grade = 7.5grade >= 9"Outstanding"Falsegrade >= 7"Very good"Truegrade >= 5"Pass"not evaluatedelse"Fail"not evaluatedprint(…) → 7.5 → Very good1. Asks from top to bottom2. The first True wins3. The rest is skipped
With grade = 7.5 only two questions are asked: the second one is True and Python jumps to the end without looking at the rest.

Two ideas come out of that table and will save you a lot of bugs:

  1. Only one block runs: the one for the first True. Even though 7.5 >= 5 is also true, nobody asks.
  2. When you reach an elif, you already know everything above was False. When Python reaches elif grade >= 7, it already knows the grade is below 9. That’s why there’s no need to write elif grade >= 7 and grade < 9:: the second half is already guaranteed. The FP1 notes insist on this: don’t re-check what’s already been checked.

Why the order of elif matters

Now back to the hook. My classifier asked grade >= 5 first. With a 9.2 that question is already True, so the program assigns “Pass” and leaves. The very good and outstanding questions are there, written down, but they can never be reached: any grade that meets them has already gone in through grade >= 5.

Why the order of elif mattersTwo grade lines from 0 to 10. With the right order, 9.2 falls into Outstanding. With grade >= 5 first, everything from 5 to 10 stays in Pass.Right order: highest first (t3)FailPassVery goodOutstanding012345678910grade = 9.2Hook order: grade >= 5 first (t0)FailPass012345678910grade = 9.2Very good and Outstanding: never reached
With grade >= 5 first, the whole stretch from 5 to 10 is taken by Pass: the 9.2 from the hook never reaches Outstanding.

It’s the trickiest mistake in the topic, because it gives no error: the program works, just wrongly. The rule to avoid it:

If you compare with…Order the conditions…Example
>= or >From the highest threshold down>= 9, >= 7, >= 5, else
<= or <From the lowest threshold up<= 12450, <= 20200, <= 35200…

You’ll use the second row in the exercises, with tax brackets. The idea is the same: the first question has to be the most demanding one, the one that lets the fewest values through.

elif is an else with an if inside

The FP1 notes present elif as a shorthand, and seeing it that way helps a lot. This program (the example from the notes, with fixed data) classifies a number as negative, zero or positive, and as even or odd:

num = -7

if num < 0:
    if num % 2 == 0:               # the remainder by 2 tells if it is even
        print(num, "is a negative even number")
    else:
        print(num, "is a negative odd number")
elif num == 0:                     # elif = an else that only contains an if
    print(num, "is zero")
elif num % 2 == 0:                 # here we already know num is positive
    print(num, "is a positive even number")
else:
    print(num, "is a positive odd number")

Output:

-7 is a negative odd number

Check: -7 < 0 is True, so it enters the first block; -7 % 2 is 1 in Python (the remainder always takes the sign of the divisor), 1 == 0 is False and it prints “odd”. The two elif lines at the bottom replace an else: that only contained another if. Without that shorthand, the code would gain one more level of indentation per case. Also notice the last elif num % 2 == 0: it doesn’t ask whether the number is positive, because if it got that far it can’t be negative or zero.

Nested conditionals

You can put an if inside another one. That’s called nesting, and it’s useful when a question only makes sense if the previous one came out a certain way. For example, asking about a driving licence only makes sense if the person is an adult:

age = 20
has_licence = False

if age >= 18:
    if has_licence:                # 2nd level: only asked to adults
        print("Can drive")
    else:
        print("Adult but no licence")
else:
    print("Under 18, cannot drive")

With age = 20 and has_licence = False:

Adult but no licence

The if has_licence: is indented 4 more spaces than the first one, so it’s only evaluated when age >= 18 was True. And its else belongs to the inner if because it sits at the same level. That rule (an else pairs with the if at its own indentation) causes one of the bugs in the exercises, so keep it in mind.

The same logic can be written without nesting, by combining both conditions with and:

age = 20
has_licence = False

if age >= 18 and has_licence:     # both conditions at once
    print("Can drive")
elif age >= 18:                   # here we already know there is NO licence
    print("Adult but no licence")
else:
    print("Under 18, cannot drive")

The output is identical:

Adult but no licence

In elif age >= 18 there’s no need to add and not has_licence: if we get there, the first condition failed, and since age >= 18 is True, what failed must have been the licence.

SituationBetter nestedBetter combined with and/or
The second question only makes sense if the first is TrueYes—
Each combination gives a different message (4 cases or more)Yes, it reads betterBecomes a very long chain
You only care about “both are true” or “not”—Yes: one line instead of four
You’re at the third level of indentationWarning signTry combining or reordering

If you find yourself writing an if inside an if inside another if, stop for a moment. There’s almost always a way to flatten it with and or with a well-ordered elif, and the code becomes readable at a glance.

and, or and not: several conditions at once

You saw them in the operators topic, but inside an if is where they do the most damage when mixed up. Imagine someone mistypes a grade and enters 12. We want to detect that it’s outside the 0 to 10 range:

grade = 12          # a typo: grades go from 0 to 10

# WRONG: with or ONE True is enough, and 12 >= 0 already is
if grade >= 0 or grade <= 10:
    print("or  → grade is valid")

# RIGHT: with and BOTH must be true
if grade >= 0 and grade <= 10:
    print("and → grade is valid")
else:
    print("and → grade out of range")

# The same, shorter: chained comparison
if 0 <= grade <= 10:
    print("chained → grade is valid")
else:
    print("chained → grade out of range")

Output:

or  → grade is valid
and → grade out of range
chained → grade out of range

The or version accepts the grade, and not because Python is broken. With or, one of the two parts being True is enough: 12 >= 0 already is, so the second part doesn’t even matter. Even worse: with or, that condition accepts any number, because every number is greater than or equal to 0, or less than or equal to 10 (or both). The diagram shows it on the number line:

and versus or on the grade lineFour bands over a line from -2 to 12. grade >= 0 covers from 0, grade <= 10 up to 10, and only from 0 to 10, and or covers the whole line, so 12 passes with or.grade >= 0True with 12grade <= 10False with 12grade >= 0 and grade <= 10False with 12grade >= 0 or grade <= 10True with 12-2024681012grade = 12
and keeps the common part (0 to 10); or keeps everything either side covers, which is the whole line: 12 gets through.
ABA and BA or Bnot A
TrueTrueTrueTrueFalse
TrueFalseFalseTrueFalse
FalseTrueFalseTrueTrue
FalseFalseFalseFalseTrue

The third form in the program, 0 <= grade <= 10, is a chained comparison. Python reads it as 0 <= grade and grade <= 10, but it looks much more like the way you’d write it in maths. For checking ranges, it’s the clearest form and the hardest one to get wrong.

and and or stop as soon as they know the answer

and and or don’t always evaluate both sides. If the first part of an and is False, the result is already False whatever the second part says, and Python doesn’t look at it. This is called short-circuit evaluation, and you can use it on purpose to protect a dangerous operation:

submissions = 0
points = 0

# and stops as soon as it finds a False: with 0 submissions it never divides
if submissions > 0 and points / submissions >= 5:
    print("Average passed")
else:
    print("No submissions or average failed")

print(points / submissions)    # without the protection of and...

Output:

No submissions or average failed
Traceback (most recent call last):
  File "short_circuit.py", line 10, in <module>
    print(points / submissions)    # without the protection of and...
          ~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero

With submissions = 0, submissions > 0 gives False and the and stops: the division points / submissions is never calculated and the program goes into the else. The last line, however, divides with no protection and the program crashes with ZeroDivisionError. Order inside the and matters: first the check that protects, then the operation that needs that protection.

The or trap with a lone string

This is the most typical or mistake in first year. You want to accept “s” and “S” as a yes, and you write what you’d say out loud:

option = "n"

# Looks like "if option is s or S", but Python reads (option == "s") or ("S")
if option == "s" or "S":
    print("Gets in even though option is 'n'")

print(option == "s" or "S")     # the or returns "S", which is True inside an if

# RIGHT: compare twice, or lowercase it and compare once
if option == "s" or option == "S":
    print("Doesn't get in")
if option.lower() == "s":
    print("Doesn't get in either")
print("End")

Output:

Gets in even though option is 'n'
S
Fin

With option = "n" it still gets in. The reason is how Python groups it: option == "s" or "S" is read as (option == "s") or ("S"). The first part is False, so the or returns the second, "S", which is a non-empty string and counts as true inside an if (more on that in the next section). The line print(option == "s" or "S") proves it: it prints S, not True or False.

if option == "s" or "S": always gets in. Each side of the or has to be a complete comparison: option == "s" or option == "S". Or better: normalise with option.lower() == "s" and compare once.

True and false without comparing: truthy and falsy

Inside an if you can put any value, not just True or False. Python turns it into a boolean with the same rules as the bool() function:

# bool() shows how Python sees each value inside an if
print(bool(0), bool(0.0), bool(""), bool([]))              # zero or empty → False
print(bool(7), bool(-1), bool("0"), bool(" "), bool([0]))  # everything else → True

submissions = []               # nothing handed in yet
if submissions:                # an empty list counts as False
    print("Has submissions")
else:
    print("Empty list: no submissions")

comment = "0"                  # careful: it is text, not the number 0
if comment:
    print("The text '0' counts as True")

Output:

False False False False
True True True True True
Empty list: no submissions
The text '0' counts as True

The rule is short: zero and empty are false; everything else is true. Values that behave like False are called falsy, and those that behave like True, truthy.

Valuebool(value)Why?
0, 0.0FalseIt’s zero
""FalseEmpty string
[]FalseEmpty list
7, -1TrueNot zero (negatives too)
"0", " "TrueStrings with one character: not empty
[0]TrueThe list has one element, even if that element is 0

That’s why if submissions: is the natural way to ask “is there any submission?”. And that’s why the string "0" counts as true: it isn’t the number zero, it’s a one-character string. Keep this in mind when you read numbers with input(), which always returns text.

In FP1 you’ll see if my_list: or if not name: a lot. It isn’t a weird shorthand: it’s Python asking “does it have anything in it?”. If it’s hard to read, translate it in your head to if len(my_list) > 0:.

The ternary operator — conditional in one line

When an if/else only exists to pick one value out of two, Python lets you write it on one line. It’s called the ternary operator or conditional expression:

grade = 6.0
result = "Pass" if grade >= 5 else "Fail"   # value_if_True if condition else value_if_False
print(result)

submissions = 1
print(f"You have {submissions} {'submission' if submissions == 1 else 'submissions'}")

Output:

Pass
You have 1 submission

It reads almost like English: "Pass" if grade >= 5 else "Fail" is “Pass if the grade is at least 5; otherwise, Fail”. The order is value if True, if condition, else value if False. The second example uses it inside an f-string to choose between “submission” and “submissions”: with submissions = 1 you get the singular.

Use it to pick simple values. If you start putting another ternary or a condition with three ands inside it, go back to the usual if/else: it takes more lines, but people can follow it.

When do I use each structure?

You need to…UseExample
Do something only if a condition holdsifWarn if the grade is below 5
Choose between two pathsif / elsePass or fail
Choose one of several mutually exclusive pathsif / elif / elseOutstanding, very good, pass, fail
Check several independent thingsSeveral ifs in a rowValidate username, age and password in a sign-up form
Ask a question that depends on anotherNested ifLicence only if they’re an adult
Pick a value in one lineTernary"error" if n == 1 else "errors"

The fourth row is the key to the practice: an if/elif chain stops at the first True, so if you want to check every field of a form you need one if per field. You’ll see it with a real sign-up validator.

Most common mistakes with Python conditionals

Mistake 1 — Missing colon

grade = 7
if grade >= 5
    print("Pass")

Python doesn’t even start running the program. This is the real message:

  File "missing_colon.py", line 2
    if grade >= 5

SyntaxError: expected ':'

Recent Python versions tell you exactly what’s missing (expected ':') and point at the end of the line with ^. Always read the last line of the error: it’s the most helpful one.

Mistake 2 — Using = instead of ==

grade = 5
if grade = 5:
    print("Five")

Real error:

  File "equals.py", line 2
    if grade = 5:
       ^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?

= assigns and == compares. Inside an if you always want to compare, and Python even suggests the fix: Maybe you meant ‘==’.

Mistake 3 — Wrong indentation

grade = 7
if grade >= 5:
print("Pass")

Real error:

  File "indentation.py", line 3
    print("Pass")
    ^^^^^
IndentationError: expected an indented block after 'if' statement on line 2

After the colon Python expects an indented block. If the next line is at the same level as the if, there’s no block and you get an IndentationError.

Mistake 4 — Wrong elif order (silent error)

It’s the one from the hook: with grade >= 5 first, the 9.2 comes out “Pass”. No error message warns you; just an output that doesn’t add up. The fix is to order from the most demanding condition to the least demanding one.

Testing a classifier only with “normal” grades. Always test the extremes and the boundaries: 0, 4.99, 5, 7, 9, 9.2 and 10. The order bug only shows up with high grades.

Mistake 5 — or with a lone string

if option == "s" or "S": is always true, because "S" on its own is truthy. Each side of the or needs its own complete comparison.

Watch it step by step in Python Tutor

pythontutor.com runs your code line by line and shows which line runs next and what value each variable has at every moment. For conditionals it’s gold: you see with your own eyes which branches get skipped. Choose Python 3, paste this program (it’s the guiding grades example: the exam is worth 60 %, coursework 40 %, and you need at least a 4 in the exam for the average to count) and click Visualize Execution:

exam_grade = 6.5
coursework_grade = 8.0

if exam_grade >= 4:                     # requirement: at least 4 in the exam
    final = 0.6 * exam_grade + 0.4 * coursework_grade
    if final >= 9:
        grade_name = "Outstanding"
    elif final >= 7:
        grade_name = "Very good"
    elif final >= 5:
        grade_name = "Pass"
    else:
        grade_name = "Fail"
else:                                   # without the 4 there is no average
    final = exam_grade
    grade_name = "Fail (exam < 4)"

print(round(final, 2), grade_name)

At the end you should see:

7.1 Very good

The maths: 0.6 × 6.5 = 3.9 and 0.4 × 8.0 = 3.2, which add up to 7.1. The round(final, 2) is a precaution: arithmetic with decimals sometimes leaves strange tails (try 0.1 + 0.2 in the console and you’ll see 0.30000000000000004), and other grades could produce one.

What to watch step by step:

  1. At the first if, the arrow goes from if exam_grade >= 4: to the final line: the condition was True and it enters the block.
  2. When final appears in the variables panel, check that it’s 7.1: it’s the calculation we just did by hand, stored before the chain of grade names looks at it.
  3. In the inner chain, the arrow stops at if final >= 9 and at elif final >= 7, and jumps straight to the print. The “Pass” and “Fail” lines and the whole outer else are never visited.
  4. Change exam_grade to 3.5 and run it again: now the arrow jumps from the first if straight to the else at the bottom, and the grade chain isn’t visited at all.

Quick summary

ConceptWhat it doesDetail people forget
ifRuns a block if the condition is TrueColon and 4 spaces of indentation
elseBlock for when the condition is FalseHas no condition
elifAnother condition if the previous ones failedOnly the first True runs
Order of elifDecides which branch wins>= highest first; <= lowest first
Nestingif inside ifThe else pairs by indentation
and / or / notCombine conditionsor with ranges tends to accept everything
Truthy / falsyAny value can be a conditionZero and empty are False; "0" is True
Ternarya if condition else bOnly for picking simple values

Three ideas to take away:

  1. In an if/elif/else chain only one block runs: the one for the first True. That’s why order is everything.
  2. To check several independent things, use several ifs, not an elif chain.
  3. Each side of an and or an or is a complete condition. 0 <= grade <= 10 for ranges, and never x == "a" or "b".

Now it’s time to write code: in the Python conditionals practice we build a sign-up validator, a discount system and a grade classifier with a menu. And when you want to repeat a decision many times (marking the whole class’s grades, for example), Python loops are waiting for you.

Next step: practicePython conditionals — practice with a sign-up validator, tier discounts and a grade classifier (and the form that only showed you one error)

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *