The previous lesson ended with a promise — that if we used on tiptoe is explained in full here — and with a diagnosis: EasyTask spots bad data, but all it knows how to do is warn about it and drop in a default value. Warning is not deciding.

A program that runs its instructions in order, from the first to the last, always does the same thing. Useful programs choose: if the data is valid they use it, if not they fix it; if the task is high priority they flag it as urgent, if not they leave it in the normal queue. That ability to choose is the first of the two control structures in this module; the other one, repeating, arrives in Loops. Here you will learn the full syntax of if, elif and else, why indentation in Python is grammar rather than decoration, how to combine conditions, and when to nest decisions or flatten them.

Contents

  1. What a decision is in a program
  2. The if: condition, colon and indentation
  3. if / else: the alternative path
  4. if / elif / else: mutually exclusive alternatives
  5. Why elif is not the same as several if statements
  6. Compound conditions with and, or and not
  7. Nested conditionals and how to flatten them
  8. The ternary operator and chained comparison
  9. EasyTask v0.5.1: deciding instead of warning
  10. Common mistakes and tips
  11. Exercises
  12. Conclusion

  1. What a decision is in a program

A decision is a point where the flow branches: a condition is evaluated and, depending on whether it is true or false, one block of instructions runs or another does. The condition is always a boolean expression of the kind you built in Operators and expressions: days > 5, priority == "high", assignee in TEAM.

flowchart TD
    A["Previous instruction"] --> B{"priority == high ?"}
    B -->|True| C["Flag as urgent"]
    B -->|False| D["Leave in normal queue"]
    C --> E["Next instruction"]
    D --> E

Three details of the diagram describe exactly what the code does: the diamond has only two exits, each branch does something different, and the two branches join back together, because after the decision the program carries on along a single path. In From problem to algorithm you drew diagrams like this in pseudocode; now we will write them in Python.

  1. The if: condition, colon and indentation

The simplest form runs a block only if a condition holds, and does nothing otherwise.

estimated_days = 12

if estimated_days > 10:
    print("Long task: worth splitting into subtasks.")
print("End of analysis.")

With estimated_days = 3 the first line is not printed and we would only see End of analysis.. The header has three mandatory pieces — the word if, the condition and the final colon, and leaving any of them out gives a SyntaxError — plus a fourth one that is not on the line: the indented block underneath.

Indentation is syntax

In most languages the block is delimited with braces { } and the indentation is decoration. In Python the indentation delimits the block. There are no braces.

priority = "low"

if priority == "high":
    print("Line A: inside the if")
    print("Line B: inside the if")
print("Line C: outside the if")

Here only line C is printed: A and B are inside the block because they are indented; C returns to the left margin and always runs, whatever priority happens to be. The official convention (the PEP 8 guide, which you will meet in Style and refactoring) is 4 spaces per level. Not 2, not 8, not tabs; VS Code turns the tab key into 4 spaces automatically. If you get it wrong, Python does not guess: leaving the block of an if empty aborts with IndentationError: expected an indented block after 'if' statement on line 1. Its variants are unexpected indent (a line indented more than it should be), unindent does not match any outer indentation level (you have closed the block at an indentation level that does not exist) and TabError (you have mixed tabs and spaces). All four are fixed by looking at the indentation, never at the logic.

The strictness pays off: in Python, well-indented code and correct code are the same thing. It cannot happen that the indentation says one thing and the braces another, which is a classic in other languages.

  1. if / else: the alternative path

else defines what happens when the condition is false. It carries no condition of its own — it means "in any other case" — and its header is simply else:.

if hours_spent <= estimated_hours:
    print("Within the hours budget.")
else:
    print("Hours budget exceeded.")

The two branches are exclusive and exhaustive: one always runs, never both, and no case is left uncovered. With a single if we can already fix a flaw in version 0.5, where empty warnings printed blank lines: it is enough to wrap the print in an if priority_warning != "":. And since the empty string is falsy (the truthiness rules from Type conversion and validation), the idiomatic form is to let the value itself act as the condition, if priority_warning:. Both forms work; the first is more explicit for a beginner.

  1. if / elif / else: mutually exclusive alternatives

When there are more than two paths, elif (short for else if) chains conditions together. They are evaluated top to bottom and only the first true one runs; the rest are not even checked.

priority = "medium"

if priority == "high":
    deadline_days = 1
elif priority == "medium":
    deadline_days = 3
elif priority == "low":
    deadline_days = 10
else:
    deadline_days = 3
    print("Unknown priority. The standard deadline applies.")

print(f"Days allowed: {deadline_days}")     # Days allowed: 3

In the flowchart, each diamond hangs off the False exit of the previous one: a condition is only evaluated if all the earlier ones failed, and every branch flows into the same point. There can be any number of elif clauses, including none; the final else is optional, but it is worth adding almost always as a safety net for the unforeseen case. And order matters. If you wrote if days > 5: followed by elif days > 15:, the second branch would be dead code: no value would ever reach it, because anything greater than 15 is also greater than 5 and the first branch takes it. The rule is to put the most restrictive conditions first.

  1. Why elif is not the same as several if statements

This is the point that causes the most confusion. Same data, two versions that differ by only four letters:

days = 20
# --- elif version ---                # --- separate ifs version ---
if days > 15:                         # if days > 15:
    category = "very long"            #     category = "very long"
elif days > 5:                        # if days > 5:
    category = "long"                 #     category = "long"
else:                                 # else:
    category = "short"                #     category = "short"
print(category)    # very long        # print(category)    # long (!)

With elif, the first true condition wins and the rest are skipped. With independent if statements each one is evaluated separately: the first assigns "very long" and the second, also true, overwrites it with "long". On top of that, the final else no longer belongs to the first if but to the second.

Aspect if / elif / else Several if statements in a row
Branches that run At most one Every one that is true
Later conditions Not evaluated once one has matched Always evaluated
Typical use Classifying one value into categories (exclusive) Checking different pieces of data (independent)

Always ask yourself: are these alternatives for the same piece of data, or independent checks? Classifying a priority as high/medium/low means alternatives: elif. Validating the title, the assignee and the days are independent checks: separate if statements, as in EasyTask.

  1. Compound conditions with and, or and not

A condition can combine several checks with the logical operators from 02-02. and requires all the parts to be true; or is happy with one; not flips the result. So if priority == "high" and not completed: selects the pending urgent items, and if priority == "low" or days > 15: the candidates for postponement.

They are evaluated with short-circuiting: in A and B, if A is false, B is never even looked at. That is not an academic detail, it is a protection tool:

if days_text.strip().isdigit() and int(days_text) > 0:
    print("Valid days")

If the text contains no digits, the first part is False and Python does not run int(days_text), which would have raised a ValueError. Swap the two parts around and the program breaks. The rule: check first whatever makes the next check safe. And remember the classic mistake from 02-02, which is expensive inside an if: if priority == "high" or "medium": is always true, because "medium" is a non-empty string and therefore truthy in its own right. The correct form is if priority in ("high", "medium"):.

  1. Nested conditionals and how to flatten them

An if can contain another one inside its block; each level adds 4 spaces of indentation.

if not completed:
    if priority == "high":
        print(f"Pending urgent item for {assignee}.")
    else:
        print("Pending task, not urgent.")
else:
    print("Task completed.")

It works, but to know which case you are in you have to hold several conditions in your head at once, and the rule of the trade is that more than two or three levels of nesting are a warning sign. Many of them can be flattened by inverting the questions and chaining elif clauses:

if completed:
    print("Task completed.")
elif priority != "high":
    print("Pending task, not urgent.")
else:
    print(f"Pending urgent item for {assignee}.")

The nested version reads like a tree and is preferable when the inner question only makes sense if the outer one is true; the flat one reads like a list of cases and is preferable when the conditions describe alternatives. Pick whichever a colleague would understand faster.

  1. The ternary operator and chained comparison

When the decision amounts only to choosing a value between two, there is a one-line shorthand, the conditional expression or ternary, with the shape value_if_true if condition else value_if_false:

completed = False
status = "Completed" if completed else "Pending"        # Pending

It is equivalent to a four-line if / else and reads almost like plain English: "status is "Completed" if completed, otherwise "Pending"". Its advantage is that, being an expression rather than a statement, it fits inside an f-string: print(f"Task {'long' if days > 10 else 'short'}") prints Task long when days is 12. Use it to choose between two simple values; if each branch has to run several instructions, or there are more than two alternatives, go back to if / elif / else. And never nest one ternary inside another: it becomes unreadable.

Chained comparison. To check a range, Python lets you chain comparisons the way you would in maths: if 1 <= days <= 365:. It is equivalent to days >= 1 and days <= 365, but reads better and evaluates days only once. The if days < 1 or days > 365 from version 0.5 is now written if not (1 <= days <= 365). It is only readable with comparisons in the same direction over the same value, and do not confuse it with in: chaining checks a continuous range, in checks membership of a catalogue.

  1. EasyTask v0.5.1: deciding instead of warning

Version 0.5 detected the problems and always responded in the same way: a default value. Now we decide for real, with three improvements. Faced with an invalid priority, if it is a recognisable synonym (urgent, critical) it is read as high, and only if it is unrecognisable is the default value applied. Faced with days out of range, we distinguish too small (raised to the minimum) from too large (lowered to the maximum), instead of replacing them with a number unrelated to what was typed. And faced with an assignee outside the team, the task goes to Marta, the coordinator, and is flagged for reassignment.

We also classify the task as urgent, normal or deferrable, and empty warnings stop printing blank lines.

# easytask.py - Alba Studio
# Version 0.5.1: the program decides, it does not just warn

WIDTH = 46
HOURS_PER_WORKDAY = 8
PRIORITIES = ("high", "medium", "low")
HIGH_SYNONYMS = ("urgent", "superurgent", "critical", "blocker")
TEAM = ("Marta", "Luis", "Nuria")
COORDINATOR = "Marta"
DAYS_MIN = 1
DAYS_MAX = 365
DEFAULT_DAYS = 5

# --- 1. Title: presence ---
title = input("Task title                 : ").strip()
title = title if title != "" else "(no title)"

# --- 2. Assignee: domain, with a reasoned reassignment ---
assignee = input("Assignee (Marta/Luis/Nuria): ").strip().capitalize()
assignee_warning = ""
needs_reassignment = False
if assignee not in TEAM:
    assignee_warning = f"'{assignee}' is not on the team. Goes to {COORDINATOR}."
    assignee = COORDINATOR
    needs_reassignment = True

# --- 3. Priority: domain, reading synonyms ---
priority = input("Priority (high/medium/low) : ").strip().lower()
priority_warning = ""
if priority in PRIORITIES:
    pass
elif priority in HIGH_SYNONYMS:
    priority_warning = f"'{priority}' is read as high priority."
    priority = "high"
else:
    priority_warning = f"'{priority}' is not a priority. 'medium' is assigned."
    priority = "medium"

# --- 4. Estimated days: type and consistency, clamped to the range ---
days_text = input("Estimated days             : ").strip()
days_warning = ""
if not days_text.isdigit():
    days_warning = f"'{days_text}' is not a number. {DEFAULT_DAYS} days are assigned."
    estimated_days = DEFAULT_DAYS
else:
    estimated_days = int(days_text)
    if estimated_days < DAYS_MIN:
        days_warning = f"{estimated_days} days is far too few. Set to {DAYS_MIN}."
        estimated_days = DAYS_MIN
    elif estimated_days > DAYS_MAX:
        days_warning = f"{estimated_days} days exceeds the maximum. Set to {DAYS_MAX}."
        estimated_days = DAYS_MAX

# --- 5. Processing: classification and output ---
estimated_hours = estimated_days * HOURS_PER_WORKDAY
completed = False
if priority == "high" and not completed:
    classification = "URGENT"
elif priority == "medium" or estimated_days <= 3:
    classification = "NORMAL"
else:
    classification = "DEFERRABLE"
status = "Completed" if completed else "Pending"
print("=" * WIDTH)
print(f"{'Title':<16}{title:>{WIDTH - 16}}")
print(f"{'Assignee':<16}{assignee:>{WIDTH - 16}}")
print(f"{'Priority':<16}{priority:>{WIDTH - 16}}")
print(f"{'Days / hours':<16}{f'{estimated_days} / {estimated_hours}':>{WIDTH - 16}}")
print(f"{'Status':<16}{f'{status}, {classification}':>{WIDTH - 16}}")
print("=" * WIDTH)
if needs_reassignment:
    print(f">> {COORDINATOR} must reassign this task.")
if assignee_warning or priority_warning or days_warning:
    print("\nIssues detected:")
    if assignee_warning:
        print(" -", assignee_warning)
    if priority_warning:
        print(" -", priority_warning)
    if days_warning:
        print(" -", days_warning)

With the inputs Book fair poster, Pedro, SUPERURGENT and 800, the output is:

==============================================
Title                         Book fair poster
Assignee                                 Marta
Priority                                  high
Days / hours                        365 / 2920
Status                         Pending, URGENT
==============================================
>> Marta must reassign this task.

Issues detected:
 - 'Pedro' is not on the team. Goes to Marta.
 - 'superurgent' is read as high priority.
 - 800 days exceeds the maximum. Set to 365.

In version 0.5, superurgent became medium (losing the user's intent) and 800 became 5 (a made-up number). Now every correction preserves the information in the original value. That is what deciding means.

The pass in the first branch is an instruction that does nothing, and it exists because Python requires every block to contain at least one instruction: it is the way of saying "there is nothing to do here, and that is deliberate". And the classification uses elif priority == "medium" or estimated_days <= 3 because a low-priority task of three days or less is better got out of the way than postponed: real business rules have exactly this shape. Even so, the loose end is more visible than ever: when the data is unrecognisable, the right thing to do would be to ask again, not invent a value. And for that we need to repeat.

Common Mistakes and Tips

Forgetting the colon. if days > 5 without : gives SyntaxError: expected ':'. The same applies to if, elif and else. And else priority == "low": does not exist either: else never takes a condition; if you need one, it is an elif.

Confusing = with ==. if priority = "high": is a SyntaxError. One assigns, the other compares; Python protects you here, but other languages do not.

Chaining if when you meant elif. The mistake from section 5, and the worst of them all because it raises no error: the result is simply different.

Leaving dead code because of the order of the conditions. if days > 5 before elif days > 15 makes the second branch unreachable. Order from the most restrictive to the most general. And writing if priority == "high" or "medium" is always true: use in ("high", "medium").

Tip: always put a final else on your elif chains. The day an unforeseen value turns up, the else will tell you instead of leaving a variable undefined.

Tip: extract long conditions into a named variable. is_urgent = priority == "high" and not completed, and then if is_urgent:. The code reads like a sentence and the condition can be printed to debug it.

Exercises

Exercise 1: Predict the output

Without running anything, say what each fragment prints.

# a)
days = 20
if days > 15:
    print("very long")
elif days > 5:
    print("long")
else:
    print("short")

# b)
days = 20
if days > 15:
    print("very long")
if days > 5:
    print("long")
else:
    print("short")

Exercise 2: Workload classifier

Write workload.py which asks for the name of a team member and the hours assigned this week, and which:

  1. Normalises and validates the name against TEAM; if it does not belong, warns and assigns it to "Marta". Validates with .isdigit() that the hours are an integer between 0 and 60; if not, warns and assigns 40.
  2. Classifies the workload with if / elif / else: "Underused" below 20, "On track" from 20 to 40, "Overloaded" from 41 to 50 and "Burnout risk" above that.
  3. Works out with a ternary the text "full-time" or "part-time" depending on whether it reaches 35 hours.
  4. If the person is overloaded and is also Marta, prints an extra warning: the coordinator cannot be the bottleneck. Finally shows an aligned card, and the issues only if there are any.

Exercise 3: Flatten a nested structure

Rewrite this structure without any nested if, keeping the same behaviour: an if title != "": contains an if assignee in TEAM:, which in turn contains an if days >= 1: with the message "Valid task"; the three else branches print, from the inside out, "Wrong days", "Wrong assignee" and "Title missing".

Solutions

Solution 1.

Output Why
a very long The first two conditions are true, but elif stops after the first one
b very long and long These are two independent if statements and both conditions are true; the else belongs to the second one

It is the same code apart from four letters, and it gives different output: two lines where the programmer expected one, with no error to give it away.

Solution 2.

# workload.py - Alba Studio
WIDTH = 42
TEAM = ("Marta", "Luis", "Nuria")
COORDINATOR = "Marta"
DEFAULT_HOURS = 40
name = input("Person (Marta/Luis/Nuria): ").strip().capitalize()
name_warning = ""
if name not in TEAM:
    name_warning = f"'{name}' is not on the team. Assigned to {COORDINATOR}."
    name = COORDINATOR

hours_text = input("Hours assigned (0-60)    : ").strip()
hours_warning = ""
if hours_text.isdigit() and 0 <= int(hours_text) <= 60:
    hours = int(hours_text)
else:
    hours_warning = f"'{hours_text}' is not valid. {DEFAULT_HOURS} h assigned."
    hours = DEFAULT_HOURS

if hours < 20:
    classification = "Underused"
elif hours <= 40:
    classification = "On track"
elif hours <= 50:
    classification = "Overloaded"
else:
    classification = "Burnout risk"

workday = "full-time" if hours >= 35 else "part-time"

print(f"{name:<18}{f'{hours} h, {workday}':>{WIDTH - 18}}")
print(f"{'Classification':<18}{classification:>{WIDTH - 18}}")

if hours > 40 and name == COORDINATOR:
    print(">> The coordinator is overloaded: share the work out.")
if name_warning or hours_warning:
    print(name_warning, hours_warning)

Two details. The hours validation makes use of short-circuiting: int(hours_text) only runs once .isdigit() has confirmed that the text is convertible, so a single condition covers both the type and the range. And in the elif chain, since each branch is only evaluated if the previous one failed, by the time we reach elif hours <= 40 we already know that hours >= 20: the conditions of an elif implicitly carry the negation of the earlier ones, and taking advantage of that simplifies code enormously.

Solution 3.

if title == "":
    print("Title missing")
elif assignee not in TEAM:
    print("Wrong assignee")
elif days < 1:
    print("Wrong days")
else:
    print("Valid task")

This form is called guard clauses: the error cases first, each with its own message, and the correct case at the end with no accumulated indentation. Adding a new validation means adding an elif, not opening another level. And the reading order matches the order of the checks, something the nested version lost: when you read "Wrong days" deep inside it, it was not obvious that the title and the assignee were already correct.

Conclusion

Your program now knows how to choose. You can write an if with its condition, its colon and its block indented by 4 spaces, and you understand that in Python indentation is grammar: IndentationError is the price of getting it wrong. You can add an else, chain elif clauses for mutually exclusive alternatives and — most importantly — tell when a problem calls for elif and when it calls for several independent if statements, because that confusion produces no error, just incorrect results. You can combine conditions with and, or and not, using short-circuiting to protect yourself from a ValueError, nest when the inner question depends on the outer one and flatten with guard clauses when it does not, choose a value in one line with the ternary and check ranges with 1 <= days <= 365.

EasyTask has moved on to version 0.5.1 and changed its attitude: it no longer silently replaces odd data, but reads what it can, clamps whatever falls outside the range, reassigns with judgement to the coordinator and classifies the work as urgent, normal or deferrable. The empty warnings are gone, exactly as we promised when closing module 2. But one decision is still beyond the program, and it is the most natural of all: when Marta types a priority that does not exist, the sensible thing is neither to correct it nor to invent one, but to ask her again. With an if that is impossible: it runs once and moves on. Nor can we register a second task without relaunching the program, or add up the days of the five tasks Marta has in her head. All of that is the same thing: repeating. That is what you will learn in Loops, where while and for will finally tie up the loose end of asking again until the data is valid.

© Copyright 2026. All rights reserved