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.
Table of Contents
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 theifline is mandatory. Without it,SyntaxError. - Indentation (4 spaces) marks the block. Every line below the
ifwith that indentation belongs to it. - The first line without indentation closes the block and always runs.
| Line | Part of the if? | With 7.5 | With 4.0 |
|---|---|---|---|
print("Pass") | Yes (4 spaces) | Runs | Skipped |
print("On to the next one!") | Yes (4 spaces) | Runs | Skipped |
print("End of program") | No (0 spaces) | Runs | Runs |
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:
| Step | Question | Result | What happens |
|---|---|---|---|
| 1 | grade >= 9 → 7.5 >= 9 | False | Moves down to the next elif |
| 2 | grade >= 7 → 7.5 >= 7 | True | Runs its block: grade_name = "Very good" |
| 3 | grade >= 5 | Not evaluated | Jumps to the end of the structure |
| 4 | else | Not 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.
Two ideas come out of that table and will save you a lot of bugs:
- Only one block runs: the one for the first
True. Even though7.5 >= 5is also true, nobody asks. - When you reach an
elif, you already know everything above wasFalse. When Python reacheselif grade >= 7, it already knows the grade is below 9. That’s why there’s no need to writeelif 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.
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.
| Situation | Better nested | Better combined with and/or |
|---|---|---|
The second question only makes sense if the first is True | Yes | — |
| Each combination gives a different message (4 cases or more) | Yes, it reads better | Becomes 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 indentation | Warning sign | Try 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:
| A | B | A and B | A or B | not A |
|---|---|---|---|---|
True | True | True | True | False |
True | False | False | True | False |
False | True | False | True | True |
False | False | False | False | True |
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.
| Value | bool(value) | Why? |
|---|---|---|
0, 0.0 | False | It’s zero |
"" | False | Empty string |
[] | False | Empty list |
7, -1 | True | Not zero (negatives too) |
"0", " " | True | Strings with one character: not empty |
[0] | True | The 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… | Use | Example |
|---|---|---|
| Do something only if a condition holds | if | Warn if the grade is below 5 |
| Choose between two paths | if / else | Pass or fail |
| Choose one of several mutually exclusive paths | if / elif / else | Outstanding, very good, pass, fail |
| Check several independent things | Several ifs in a row | Validate username, age and password in a sign-up form |
| Ask a question that depends on another | Nested if | Licence only if they’re an adult |
| Pick a value in one line | Ternary | "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:
- At the first
if, the arrow goes fromif exam_grade >= 4:to thefinalline: the condition wasTrueand it enters the block. - When
finalappears in the variables panel, check that it’s7.1: it’s the calculation we just did by hand, stored before the chain of grade names looks at it. - In the inner chain, the arrow stops at
if final >= 9and atelif final >= 7, and jumps straight to theprint. The “Pass” and “Fail” lines and the whole outerelseare never visited. - Change
exam_gradeto3.5and run it again: now the arrow jumps from the firstifstraight to theelseat the bottom, and the grade chain isn’t visited at all.
Quick summary
| Concept | What it does | Detail people forget |
|---|---|---|
if | Runs a block if the condition is True | Colon and 4 spaces of indentation |
else | Block for when the condition is False | Has no condition |
elif | Another condition if the previous ones failed | Only the first True runs |
Order of elif | Decides which branch wins | >= highest first; <= lowest first |
| Nesting | if inside if | The else pairs by indentation |
and / or / not | Combine conditions | or with ranges tends to accept everything |
| Truthy / falsy | Any value can be a condition | Zero and empty are False; "0" is True |
| Ternary | a if condition else b | Only for picking simple values |
Three ideas to take away:
- In an
if/elif/elsechain only one block runs: the one for the firstTrue. That’s why order is everything. - To check several independent things, use several
ifs, not anelifchain. - Each side of an
andor anoris a complete condition.0 <= grade <= 10for ranges, and neverx == "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 →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)