if / elif / else
Conditions, nesting, and conditional expressions.
What you will be able to do
- Write if, if/else, and if/elif/else, with correct colons and indentation
- Explain why only one branch of a chain runs, and why order matters
- Tell an elif chain apart from a series of separate ifs
- Flatten nested conditions using guard clauses
- Use a conditional expression where it reads well, and avoid it where it does not
- Report which rule failed, rather than only that something did
The idea, in plain English
Everything so far has run top to bottom, every line, every time. A conditional is the first thing that changes that: it lets a program take one path or another depending on what is true when it runs.
The machinery is small. `if` takes a condition and a block; `else` catches everything the if did not; `elif` inserts another condition between them. The colon ends the header line, and the indented lines underneath are the block.
The part worth care is evaluation order. Python tests the conditions from top to bottom, runs the first block whose condition is truthy, and skips every other branch in the chain - including ones that would also have matched. Whether your grade boundaries work at all depends on the order you wrote them in.
Conditions themselves you already know: comparison from lesson 8, truthiness from lesson 9. This lesson is about the statement built around them.
Worked example: Turning a mark out of 100 into a grade, in the right order.
Only one branch runs, and order decides which
Python evaluates the conditions in order and stops at the first one that is truthy. The rest of the chain is never tested, even where it would also have matched.
That is why a grade ladder must run from the narrowest boundary to the widest. With `if score >= 50` first, a score of 95 matches immediately and prints "Pass" - the `elif score >= 90` below it is unreachable for every value that could satisfy it.
The rule: order from most specific to most general. Any condition that is a subset of one above it will never run.
if >= 90 / elif >= 50Prints "A". Correct - the tighter bound is tested first.if >= 50 / elif >= 90Prints "Pass". The second branch is dead code; no score can ever reach it.Two separate ifsBoth run. They are independent statements, not a chain.No branch matchesNothing happens, unless there is an else. Python does not warn you.elseOptional, and always last. It has no condition because it is everything left over.Watch out: Python will not tell you a branch is unreachable. A wrongly ordered chain is valid code that quietly produces the wrong answer.
elif is not the same as a second if
This is the distinction people miss. An elif chain is one statement with several branches, and exactly one of them runs. A series of separate ifs is several statements, and every one of them is tested.
Sometimes independent ifs are exactly right - you want to collect every validation error, not stop at the first. Sometimes they are a bug, because two branches both assign to the same variable and the last one wins.
Ask yourself: are these alternatives, or are they separate questions? Alternatives take elif.
if / elifOne statement. The first match runs, the second is skipped. Use for alternatives.if / ifTwo statements. Both run. Use when the questions are independent.Collecting errorsSeparate ifs - you want every problem, not just the first.Picking a gradeelif - a score has exactly one grade.Guard clauses beat nesting
Four nested ifs produce a staircase of indentation, and the reader has to hold four conditions in their head to know what the innermost line requires. The deeper the nesting, the harder it is to see which case you are in.
The fix is to invert the tests and leave early. Check the things that disqualify, handle each one immediately, and let the happy path fall out at the bottom with no indentation at all. This is called a guard clause, and it is the single most useful refactor for conditional code.
It also gives you something valuable for free: because each failure is handled separately, you can say which rule failed rather than just returning False.
Tip: Whenever an if block is the entire body of a function, try inverting it. `if not x: return` at the top usually reads better than wrapping everything in `if x:`.
Conditional expressions, and when to stop
When both branches do nothing but assign to the same name, a conditional expression says it in one line: `category = "Adult" if age >= 18 else "Minor"`. The condition sits in the middle, which reads oddly for about a day.
It is an expression, so it produces a value and can go anywhere a value can - inside an f-string, as a function argument, in a list comprehension.
Chaining them is where it goes wrong. Three or four stacked with else clauses is a grade ladder written sideways, and it is harder to read than the if/elif version and harder to change. Two outcomes: expression. Three or more: statement.
Syntax and examples
age = 25
# if alone - nothing happens when the condition is false
if age >= 18:
print("Adult")
# if/else - exactly one of the two runs
if age >= 18:
print("Adult")
else:
print("Minor")
# if/elif/else - the first match wins
if age >= 65:
print("Senior")
elif age >= 18:
print("Adult")
else:
print("Minor")age = 25
# if age >= 18 <- SyntaxError: expected ':'
# if age >= 18:
# print("Adult") <- IndentationError: expected an indented block
if age >= 18:
print("Adult") # in the block
print("Can vote") # same block - same indentation
print("Always runs") # outside the block
# A block cannot be empty. Use pass when there is nothing to do yet.
if age < 0:
passscore = 95
# Wrong: the broad condition comes first, so the narrow one is unreachable
if score >= 50:
print("Pass") # prints this
elif score >= 90:
print("Excellent") # never reached by any score
# Right: narrowest boundary first
if score >= 90:
print("Excellent") # prints this
elif score >= 50:
print("Pass")score = 95
# One statement, one branch runs
if score >= 90:
print("A")
elif score >= 50:
print("Pass")
# -> A
# Two statements, both run
if score >= 90:
print("A")
if score >= 50:
print("Pass")
# -> A
# -> Pass
# Separate ifs are right when you want every problem, not just the first
errors = []
username, password = "", "x"
if not username.strip():
errors.append("username is required")
if len(password) < 8:
errors.append("password is too short")
print(errors) # both reporteddef can_access_nested(user):
if user is not None:
if user["active"]:
if user["subscribed"]:
return "access granted"
else:
return "subscription required"
else:
return "account inactive"
else:
return "not signed in"
def can_access(user):
# Handle each disqualifying case and leave. The happy path ends up
# at the bottom, unindented, and the reason is specific either way.
if user is None:
return "not signed in"
if not user["active"]:
return "account inactive"
if not user["subscribed"]:
return "subscription required"
return "access granted"
print(can_access({"active": True, "subscribed": False})) # subscription requiredage, role, score = 25, "teacher", 85
email = "user@example.com"
if 18 <= age <= 60: # chained comparison
print("Working age")
if role in ("admin", "teacher"): # membership
print("Can manage courses")
if "@" in email: # substring - a weak check, but a check
print("Looks like an email")
user = {"name": "Chandu"}
if user.get("role") == "admin": # get() avoids a KeyError
print("Administrator")
else:
print("Not an administrator")
if 0 <= score <= 100:
print("Valid score")age = 20
category = "Adult" if age >= 18 else "Minor"
print(category) # Adult
# It is an expression, so it goes anywhere a value goes
print(f"Status: {'Active' if age else 'Unknown'}")
# Two outcomes: fine. Four: write the statement instead.
score = 82
# grade = "A" if score >= 90 else "B" if score >= 75 else "C" if score >= 60 else "F"
if score >= 90:
grade = "A"
elif score >= 75:
grade = "B"
elif score >= 60:
grade = "C"
else:
grade = "F"
print(grade) # Bstudent_name = "Ravi"
marks = 82
if not 0 <= marks <= 100:
print("Marks must be between 0 and 100")
else:
if marks >= 90:
grade = "A"
elif marks >= 80:
grade = "B"
elif marks >= 70:
grade = "C"
elif marks >= 60:
grade = "D"
elif marks >= 40:
grade = "E"
else:
grade = "Fail"
print(f"Student: {student_name}")
print(f"Grade: {grade}")
print(f"Result: {'Pass' if marks >= 40 else 'Fail'}")Tip: A long condition is easier to read when it is given a name first: can_enrol = is_live and (has_sub or is_free). The if statement then reads as a sentence.
The pieces of a conditional
ifRuns its block when the condition is truthy. Required; everything else is optional.
if age >= 18:
elifAnother condition, tested only if every branch above it failed. As many as you like.
elif age >= 13:
elseRuns when nothing above matched. No condition, and always last.
else:
:Ends every header line. Leaving it off is a SyntaxError pointing at the next line.
if x:
indentationFour spaces marks the block. It is syntax, not style.
print("in")passA do-nothing statement, for when a block must exist but has nothing in it yet.
if x:\n pass
A if C else BA conditional expression - produces a value rather than running a block.
x = "y" if c else "n"
Conditions worth reaching for
All from lessons 8 and 9 - these are the forms that read best inside an if.
if value:Presence or emptiness. Not "is it correct".
if users:
if not value:Empty, zero, or missing - all of them. Be sure you want all three.
if not name:
if value is None:Specifically missing, as distinct from empty or zero.
if score is None:
if a <= x <= b:A range, in one chained comparison.
if 0 <= score <= 100:
if x in (...):One of several options, without repeating the variable.
if role in ("admin", "teacher"):if a and (b or c):Mixed logic - parenthesise it even when precedence agrees.
if live and (sub or free):
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“s=95 if s>=50: print("Pass") elif s>=90: print("Excellent")”
“s=95 if s>=90: print("A") if s>=50: print("Pass")”
“x=5 if x>10: print("big") print("done")”
“n=0 print(f"{'some' if n else 'none'}")”
What usually goes wrong
Every header line - if, elif, else - ends in a colon. The error points at the following line, which is why it takes beginners so long to spot.
✗ if age >= 18
print("Adult")✓ if age >= 18:
print("Adult")Indentation is how Python knows which lines belong to the condition. An unindented line after the colon is an IndentationError.
✗ if age >= 18:
print("Adult")✓ if age >= 18:
print("Adult")The first truthy branch wins and the rest are skipped. A wide condition at the top makes every narrower one below it unreachable - and Python will not warn you.
✗ if score >= 50:
grade = "Pass"
elif score >= 90:
grade = "Excellent"✓ if score >= 90:
grade = "Excellent"
elif score >= 50:
grade = "Pass"Separate ifs are separate statements and all of them run. When they assign to the same variable, the last matching one silently wins.
✗ if score >= 90:
grade = "A"
if score >= 50:
grade = "Pass" # overwrites "A"✓ if score >= 90:
grade = "A"
elif score >= 50:
grade = "Pass"Each level adds a condition the reader has to carry. Invert the tests, return early, and the happy path ends up unindented at the bottom.
✗ if user:
if user.active:
if user.paid:
allow()✓ if user is None: return
if not user.active: return
if not user.paid: return
allow()Redundant, and wrong for truthy values that are not equal to True. The bare name is the condition.
✗ if is_active == True:✓ if is_active:Two outcomes read well as an expression. Four stacked with elses is a grade ladder written sideways - harder to read and harder to change.
✗ g = "A" if s>=90 else "B" if s>=75 else "C" if s>=60 else "F"✓ if s >= 90:
g = "A"
elif s >= 75:
g = "B"
...Best practices
- Order branches from most specific to most general.
- Use elif for alternatives and separate ifs for independent questions.
- Prefer guard clauses to nesting - handle each failure and leave.
- Report which rule failed, not just that something did.
- Name a complicated condition before testing it.
- Use a conditional expression for two outcomes, an if/elif statement for three or more.
- Put the validation check before the branching, so the branches can assume good input.
- Use pass as a deliberate placeholder, and do not leave it in finished code.
Practice
Write these yourself before opening anything. Getting them wrong first is most of how this sticks.
Given age = 21, print "Adult" when the age is 18 or above and "Minor" otherwise.
Show solutionHide solution
age = 21
if age >= 18:
print("Adult") # Adult
else:
print("Minor")Given number = -10, print "Positive", "Negative", or "Zero".
Show hintHide hint
Three outcomes means two conditions and an else.
Show solutionHide solution
number = -10
if number > 0:
print("Positive")
elif number < 0:
print("Negative") # Negative
else:
print("Zero")Given score = 86, print a grade: 90+ A, 80-89 B, 70-79 C, 60-69 D, below 60 F.
Show hintHide hint
Highest boundary first. Each elif only runs when everything above failed, so you do not need an upper bound.
Show solutionHide solution
score = 86
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B" # B
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(grade)Given username = "admin" and password = "secret", print "Login successful" only when both match. Otherwise print "Invalid credentials".
Show solutionHide solution
username = "admin"
password = "secret"
if username == "admin" and password == "secret":
print("Login successful")
else:
print("Invalid credentials")Given is_logged_in = True, has_subscription = False, is_public_course = True, allow access when logged in and either subscribed or the course is public.
Show hintHide hint
The parentheses matter - and binds tighter than or.
Show solutionHide solution
is_logged_in = True
has_subscription = False
is_public_course = True
if is_logged_in and (has_subscription or is_public_course):
print("Access granted") # Access granted
else:
print("Access denied")Rewrite this nested version with guard clauses: if user: if user["active"]: if user["paid"]: print("ok").
Show hintHide hint
Invert each test and leave early. Put it in a function so you can return.
Show solutionHide solution
def check(user):
if user is None:
return "no user"
if not user["active"]:
return "inactive"
if not user["paid"]:
return "unpaid"
return "ok"
print(check({"active": True, "paid": False})) # unpaidCollect every validation error for username = "" and password = "abc" - do not stop at the first.
Show hintHide hint
Separate ifs, not a chain.
Show solutionHide solution
username = ""
password = "abc"
errors = []
if not username.strip():
errors.append("username is required")
if len(password) < 8:
errors.append("password must be at least 8 characters")
if errors:
for error in errors:
print("-", error)
else:
print("All good")Course enrolment checker
Decide whether a learner can enrol, and when they cannot, say exactly which rule stopped them. Guard clauses make this straightforward - each failure is handled where it is detected.
- Deny a blocked user before checking anything else
- Require the learner to be signed in
- Allow a public course without a subscription; require one for a private course
- Require the prerequisite where the course has one
- Return the specific reason for a denial, never just "denied"
- Run every case below and check the reason is the one you expect
def check_enrolment(logged_in, blocked, subscribed, public, prereq_done):
...
cases = [
# logged_in, blocked, subscribed, public, prereq_done
(True, False, True, False, True),
(True, False, False, True, True),
(True, False, False, False, True),
(True, True, True, True, True),
(False, False, True, True, True),
(True, False, True, False, False),
]Show one solutionHide solution
def check_enrolment(logged_in, blocked, subscribed, public, prereq_done):
# Guard clauses, in priority order. Blocked outranks everything, so it
# is tested first; the happy path falls out at the bottom unindented.
if blocked:
return False, "User is blocked"
if not logged_in:
return False, "Please log in"
if not public and not subscribed:
return False, "Subscription required"
if not prereq_done:
return False, "Prerequisite course must be completed"
return True, "Enrollment successful"
cases = [
(True, False, True, False, True),
(True, False, False, True, True),
(True, False, False, False, True),
(True, True, True, True, True),
(False, False, True, True, True),
(True, False, True, False, False),
]
for case in cases:
ok, reason = check_enrolment(*case)
print(f"{'PASS' if ok else 'FAIL':<5} {reason}")
# PASS Enrollment successful
# PASS Enrollment successful
# FAIL Subscription required
# FAIL User is blocked
# FAIL Please log in
# FAIL Prerequisite course must be completedKey points
- if runs its block when the condition is truthy; else catches everything else; elif adds another condition between them.
- The colon ends the header line and indentation marks the block - both are syntax.
- Conditions are tested top to bottom, and the first truthy one wins.
- Only one branch of an if/elif/else chain ever runs.
- Order from most specific to most general, or the narrow branches become unreachable.
- Python does not warn about an unreachable branch - it is valid code with the wrong answer.
- An elif chain runs one branch; separate if statements all run.
- Guard clauses - invert the test and leave early - beat deep nesting and give you specific failure reasons.
- A block cannot be empty; use pass when there is nothing to do yet.
- A conditional expression suits two outcomes; three or more want an if/elif statement.
- Name a complex condition first so the if statement reads as a sentence.
Quick check before you move on
Interview questions
How would you keep a complicated set of business rules readable?
Name the intermediate conditions so the final test reads as a sentence, use guard clauses so each disqualifying case is handled where it is detected, and return a reason rather than a bare boolean. Once the rules outgrow a function - many combinations, or rules that change often - move them into data: a table of conditions, or a dispatch dict mapping a key to a handler, so adding a rule is adding a row rather than another branch.
What are the risks of deeply nested conditionals?
Every level adds a condition the reader has to carry, and the meaning of the innermost line depends on all of them. It also makes the error cases hard to find, because each else is far from its if. Nesting is a common source of missed branches, and it is a reliable signal that a function is doing more than one thing.
When would you replace an if/elif chain with a dictionary?
When every branch tests the same variable for equality and does something similar - dispatching on a role, a status code, a command name. A dict mapping value to handler turns the chain into a lookup, which is constant time, easy to extend, and easy to test. Keep the chain when the conditions are genuinely different questions or involve ranges rather than equality.
How does short-circuit evaluation interact with an if statement?
The condition is an ordinary expression, so it short-circuits as usual - `if user is not None and user.active:` never touches the attribute when user is None. That is why the order of the operands is part of the correctness of the condition, not a style choice.
When is a conditional expression the right tool?
When both branches produce a value for the same purpose and the whole thing fits comfortably on one line - a default, a label, a formatting choice. Once the branches have side effects, or there are more than two, a statement is clearer. The test is whether a reader can take it in without re-reading.
Python has no switch statement. What do you use instead?
An if/elif chain for ranges and mixed conditions, a dict for dispatch on a single value, and - since 3.10 - match/case for structural patterns, where you are destructuring the shape of the data rather than testing one value. match is next lesson.
Quiz
- 1.
What is the purpose of elif?
- 2.
How does Python evaluate an if/elif/else chain?
- 3.
Why does condition order matter?
- 4.
What is the difference between an elif chain and separate if statements?
- 5.
What is a guard clause?
- 6.
Why must a block use pass rather than being left empty?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned