Modules/Python/ Making Decisions

If, Elif and Else

Run one block or another, depending on what is true. Indentation is not decoration here - it is how Python knows what belongs to the branch.

Overview

Only the first match runs

Python evaluates each condition in turn from the top. The moment one is true, that block runs and every remaining branch is skipped — including ones that would also have been true. The else runs only if nothing matched.

if score >= 90:
    grade = "A"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

A score of 95 matches the first condition and stops. It never reaches the second, even though 95 is also ≥ 70. This exclusivity is what makes the chain a decision rather than a list of checks.

Only the first matching branch runs

Python checks each condition top to bottom and stops at the first true one. else catches everything left over.

Python 3
Output

                            

Indentation is the block

The indented lines belong to the branch. The unindented line runs either way - change the temperature and see which lines move.

Python 3
Output

                            

Reading a branch

The colon at the end of the if line is required, and the body below it must be indented - four spaces is the convention.
elif is "else if". You can have as many as you like; else is optional and there can only be one.
Only the first true branch runs. Once one matches, the rest are skipped entirely - so order your conditions from most specific to least.
A block with nothing in it is a SyntaxError. If you want a branch that does nothing yet, write pass.

If, Elif and Else: A Practical Guide

A condition is checked, one branch runs, and the rest are skipped entirely. The order you write the branches in is part of the logic, not a matter of style.

The shape of a decision

An if statement runs a block only when a condition is true. elif adds further conditions to test if the earlier ones failed, and else catches everything remaining.

temperature = 18

if temperature > 25:
    print("hot")
elif temperature > 15:
    print("mild")
elif temperature > 5:
    print("cool")
else:
    print("cold")

Two structural rules matter. The colon ends the condition, and the indentation defines the block — in Python, indentation is syntax rather than style, and mixing tabs and spaces is an error rather than an inconsistency.

Only one branch of an if/elif/else chain ever runs: the first whose condition is true. Once one matches, the rest are not even evaluated.

Order is logic, not layout

Because the first match wins, the order of the branches is part of the meaning. Reorder them and you change the program.

score = 95

if score > 50:
    grade = "pass"          # this catches 95 first
elif score > 90:
    grade = "distinction"   # unreachable for any passing score

Nobody ever gets a distinction here, and Python will not warn you — both branches are valid code. The fix is to test the most specific condition first:

if score > 90:
    grade = "distinction"
elif score > 50:
    grade = "pass"
else:
    grade = "fail"

Compare that with separate if statements, where every condition is tested and several blocks can run:

if score > 90:
    print("distinction")
if score > 50:
    print("pass")           # both print for a score of 95

Sometimes that is what you want. Usually it is not, and using elif is how you say "these are alternatives".

Truthiness, and the idioms it produces

Any value can be used as a condition, not just True and False. Python treats these as false:

False  None  0  0.0  ""  []  {}  ()  set()

Everything else is true, including -1, "False" and [0].

That gives the idiomatic way to test for emptiness:

if items:                  # idiomatic
if len(items) > 0:         # works, but noisier
if items != []:            # works, and nobody writes this

One important exception: when a value could legitimately be 0 or "", truthiness cannot distinguish it from "missing". Test explicitly:

if quantity is not None:   # 0 is a real quantity
    process(quantity)

That distinction — between "empty" and "absent" — is behind a large share of real-world bugs in every language.

Exploration guide

  1. Watch the skip. Run the example with a value that satisfies the first condition, then add a print inside the second branch. It never appears — the branch is not merely false, it is never evaluated.
  2. Break it by reordering. Swap the first two conditions and run again with the same input. You get a different, wrong answer from identical logic in a different order.
  3. Drop the else. Remove the final branch and pass a value matching nothing. Nothing happens and no error is raised, which is the usual source of an unset variable further down.
  4. Test truthiness directly. Try an empty string, an empty list and 0 as the condition. All three take the else branch.

A worked example: the order that breaks it

The shadowing bug is easier to believe when both versions run side by side:

def grade_wrong(score):
    if score > 50:
        return "pass"
    elif score > 90:
        return "distinction"
    return "fail"


def grade_right(score):
    if score > 90:
        return "distinction"
    elif score > 50:
        return "pass"
    return "fail"


for s in [95, 60, 20]:
    print(s, grade_wrong(s), grade_right(s))
95 pass distinction
60 pass pass
20 fail fail

The two functions agree on 60 and 20 and disagree on 95, which is exactly what makes this class of bug survive testing. Every ordinary value gives the right answer; only the top band is wrong, and only if somebody tests it.

There is no error, no warning, and no unreachable-code diagnostic — both branches are valid Python and the second is genuinely reachable, just never by any score that also clears 50. A linter cannot see it because deciding whether one condition implies another is not something a linter attempts.

The habit that prevents it: when the conditions are thresholds on one value, write them in strict order — either descending or ascending — and read down the chain checking that each boundary is tighter than the one below. Anything out of sequence is a bug or wants a comment saying why not.

Every branch should leave the same things defined

A chain that assigns a variable has to assign it on every path, and the failure when it does not is NameError a long way from the cause.

if score > 90:
    grade = "distinction"
elif score > 50:
    grade = "pass"

print(grade)          # NameError when score is 20

There is no else, so a low score matches nothing, grade is never created, and the failure surfaces at the print. It is not the print that is wrong.

Three ways to close it, in order of preference. Add an else that covers everything remaining, which is usually the honest fix because it forces you to decide what the leftover case means. Assign a default before the chain, which is fine when there genuinely is a sensible default and worse when it hides a case you had not thought about. Or restructure as early returns in a function, where each branch returns and there is no variable to leave unset.

The same applies to more than one variable. A chain where the first branch sets two names and the second sets one leaves the code below depending on which branch ran, which is the kind of thing that works until the data changes.

Writing a condition that reads

A chain is only as clear as its conditions, and three habits do most of the work.

Name a compound condition. When a branch tests three things, is_eligible = user.active and not user.fines and book.available on the line above turns the if into a sentence and gives the combination a meaning rather than only a value.

Prefer positives. if not is_disabled asks the reader to hold two negations. if is_enabled does not. When both a positive and a negative name are available, the positive one usually produces the shorter chain.

Use chained comparisons for ranges. if 0 <= score <= 100 reads as a range and evaluates score once. Written as score >= 0 and score <= 100 it says the same thing with more to parse and the value mentioned twice.

The underlying test is whether someone can say what the condition *means* rather than what it *checks*. if len(basket.items) > 0 and user.credit >= basket.total checks two things; if can_check_out means one.

When a chain wants to be something else

An if/elif chain is right for a handful of alternatives. Past that, three other shapes usually say it better.

A dictionary, when every branch compares one value against a constant. HANDLERS = {"add": do_add, "get": do_get} followed by a lookup replaces the chain with data. Adding a case becomes one entry rather than another branch, the valid options can be listed, and the same table can drive a help message. This does not work when the conditions are ranges or combinations — a dictionary can only ask "is it equal to this".

A table of thresholds, when the branches are bands on one number. A list of (cutoff, label) pairs walked in order does what a long grading chain does, and it puts the boundaries in one place where they can be checked, displayed and tested without touching the logic.

**match, when the branching is on the shape of the data.** Inspecting a parsed message or a command, where the alternative is a stack of type checks and key lookups, is what pattern matching was added for.

The signal for all three is repetition: if every branch of a chain has the same shape and differs only in a value, that value wants to be data. A chain whose branches genuinely do different things is a chain, and should stay one.

What Python does not have

Two absences surprise people arriving from other languages, and both are deliberate.

No switch statement, for thirty years. The argument was that an if/elif chain already does the job with less machinery and no fall-through bugs. match, added in 3.10, is not a switch either — it matches structure rather than comparing a value, and for plain equality against constants a dictionary is still the better tool.

**No ternary operator spelled ?:.** Python's conditional expression is value_if_true if condition else value_if_false, using words rather than punctuation and putting the common case first. It is longer to type and reads as a sentence, which was the trade the language chose everywhere.

There is also no assignment inside a condition by accident: if x = 1 is a SyntaxError rather than a silent assignment, which removes a whole category of C bug. The walrus := added deliberate assignment in an expression, and its distinct spelling is exactly so that the accidental version stays impossible.

Common mistakes

  • Using = instead of ==. Python raises a SyntaxError here rather than silently assigning, which is one of the friendlier things about it.
  • Getting the branch order wrong, making a specific case unreachable behind a general one.
  • Testing if x == True. Write if x. And if x is True is different again, and almost never what you want.
  • Deep nesting. Four levels of if is a sign to use early returns or guard clauses instead.
  • Forgetting that 0 and "" are falsy when they are legitimate values. Use is not None.
  • Mixing tabs and spaces, which produces an indentation error that is invisible on screen. Configure your editor to insert spaces.

Key takeaway

An if/elif/else chain evaluates conditions top to bottom and runs exactly one branch, so ordering is part of the logic — put specific conditions before general ones or the general one will shadow them. Conditions need not be comparisons, since empty containers, zero and None are all falsy; the exception worth remembering is that a meaningful zero must be tested against None explicitly rather than by truthiness.

Comparisons and boolean operators

x == y      # equal in value
x != y      # not equal
x < y       # less than       (also <=, >, >=)
x is y      # the same object
x in items  # membership

Python allows chained comparisons, which read exactly as they do in mathematics:

if 0 <= score <= 100:      # instead of score >= 0 and score <= 100

and, or and not combine conditions, and they short-circuit: and stops at the first false operand, or at the first true one. That is not just an optimisation — it is how you write safe guards:

if user is not None and user.is_admin:      # never evaluates user.is_admin
    ...                                      # when user is None

Reverse those two and the code raises AttributeError on the very case the check was meant to protect against.

and and or also return one of their operands rather than a boolean, which is why name = given_name or "Anonymous" works as a default.

The conditional expression and the match statement

For a simple either/or value, a one-line conditional expression is often clearer than four lines of if:

status = "adult" if age >= 18 else "minor"

Keep it to genuinely simple cases; nested conditional expressions are hard to read.

Python 3.10 added match, for dispatching on structure rather than on a chain of comparisons:

match command.split():
    case ["go", direction]:
        move(direction)
    case ["take", *items]:
        pick_up(items)
    case ["quit"]:
        exit()
    case _:
        print("Unknown command")

It is not a C-style switch — it destructures values and binds names, which is what makes it worth having. For plain equality against a handful of constants, a dictionary lookup is usually simpler than either.

Can a condition span several lines? Yes, inside brackets. Wrapping a long condition in parentheses lets you break it across lines without a backslash.

Is elif different from a nested else: if? Only in indentation — they behave identically. elif keeps the chain flat, which is the entire reason it exists.

Should every chain end with else? When the chain assigns a value, yes — otherwise some input leaves it unset. When each branch acts and doing nothing is a valid outcome, no.

Does the order of conditions affect speed? Slightly — the first true one stops the chain, so putting the common or cheap case first saves work. Correctness comes first, and specific before general.

Recap in one screen

  • Only the first true branch in an if/elif/else chain runs; separate ifs all run.
  • Branch order is part of the logic — specific conditions before general ones.
  • Falsy values are False, None, 0, 0.0, "", [], {}, (), set().
  • and/or short-circuit, which is how if user and user.is_admin stays safe.
  • Use x if cond else y for simple values, and match for structural dispatch.

Check yourself

0 of 3

Answer without scrolling back up.

  1. score = 95, and the branches check >= 70, then >= 80, then >= 90 in that order. What prints?

  2. What decides which lines belong to an if branch?

  3. How many else clauses can one if statement have?

Cheat sheet

If, Elif and Else

Run one block or another, depending on what is true. Indentation is not decoration here - it is how Python knows what belongs to the branch.

PYTHON · vizlearn.in/python/if_elif_else.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.