So far EasyTask has been a one-act script: it launches, asks questions, prints a card and dies. If Marta gets the priority wrong, she cannot correct it: she has to run the program again and type everything in from scratch. If she wants to see the card again, same story. A program like that can be demonstrated, but it cannot be used.

What separates a script from an application is a surprisingly simple pattern: the main loop. The application shows what it can do, waits for an order, carries it out and shows itself again, over and over, until the user decides to leave. It is the skeleton of nearly all the interactive software you have ever used, from a cash machine to a text editor.

The best part is that you already have all the pieces: while True and break from the previous lesson, the insistent validation from 03-02, the dispatching with if/elif or match from 03-01 and 03-03. This lesson simply puts them together, and with them closes the module.

Contents

  1. Anatomy of the main loop
  2. while True with break, and the flag variant
  3. Dispatching the chosen option
  4. Designing a menu that makes sense
  5. Validating the option read
  6. Confirmations for destructive actions
  7. EasyTask v0.6: the complete application
  8. Common mistakes and tips
  9. Exercises
  10. Module conclusion

  1. Anatomy of the main loop

An interactive menu always repeats the same five-step cycle.

flowchart TD
    A["Show the menu"] --> B["Read the option"]
    B --> C{"Valid option ?"}
    C -->|False| B
    C -->|True| D{"Is it the quit option ?"}
    D -->|True| F["Say goodbye and finish"]
    D -->|False| E["Run the action"]
    E --> G["Pause"]
    G --> A

The five steps, with their translation into code:

Step What it does What it is built with
Show Paints the available options Several print() calls
Read Collects the user's choice input()
Validate Rejects anything that is not an option Validation while (03-02)
Run Does the work requested if/elif or match (03-01, 03-03)
Return Goes back to the start The loop itself

And a golden rule: the loop must not be able to end by accident. The only way out is the quit option; a user's mistake cannot close the application, because in a real application that means losing work.

  1. while True with break, and the flag variant

The most direct form is the controlled infinite loop you saw in 03-03: while True: goes round indefinitely and the quit option runs a break.

while True:
    print("1. View the task")
    print("2. Quit")
    option = input("Option: ").strip()

    if option == "1":
        print("The task card would go here.")
    elif option == "2":
        print("See you later.")
        break

There is a second form, with a boolean flag governing the condition: you initialise running = True before the loop, the header becomes while running: and the quit option does running = False instead of the break. Both are correct and you will see both in professional code.

while True + break running flag
Exit condition Hidden in the body Visible in the header
Immediate exit Yes: it cuts on the spot No: it finishes the current round
Several exit points One break per place Set the flag to False
Readability Very direct More declarative

The practical difference matters: with break the loop is cut at that very instant; with the flag, the rest of the body of the current round does run before the condition is checked again. If there is a pause or a message after the dispatch, with the flag they will be seen on the way out too. We will use break in EasyTask, which is the more common choice.

  1. Dispatching the chosen option

Dispatching means steering execution towards the block that matches the chosen option. With if/elif it is immediate: if option == "1":elif option == "2": … and a final else for anything unrecognised. But since we are comparing one value against specific alternatives, this is exactly the scenario match was designed for:

match option:
    case "1":
        print("Register task")
    case "2":
        print("View card")
    case "3" | "c":
        print("Change priority")
    case "5" | "q" | "quit":
        print("See you later")
    case _:
        print("Option not recognised")

Notice the advantage of the | pattern: it lets you accept shortcuts naturally, so the user can type 5, q or quit for the same thing. With if/elif you would have to write option in ("5", "q", "quit"), which also works but reads worse when there are many options.

One important warning: however nicely the match of a menu reads, the break that leaves the loop cannot be replaced by anything in the match. break still belongs to the while, and it works just the same inside a case.

  1. Designing a menu that makes sense

The menu is your program's interface, and a badly presented menu turns a correct application into an unusable one. Six rules are enough:

  • Number the options and use consecutive numbers from 1. Typing a number is faster and less error-prone than writing a word.
  • Describe actions, not nouns. "Change the priority" says what is going to happen; "Priority" does not.
  • The quit option, always visible and always last. The user must be able to leave without having to guess how.
  • Separate the menu visually from the result with lines of dashes or equals signs. Remember the trick from lesson 02-03: print("=" * WIDTH).
  • Pause before repainting. If the menu is redrawn immediately, the result of the action disappears from view before there is time to read it. The classic solution is an input("Press Enter to continue...") whose value is ignored: it is only there to wait.
  • Repeat the range in the message: Choose an option (1-5): guides far better than Option:.
WIDTH = 46

print("=" * WIDTH)
print(f"{'EasyTask v0.6 - Alba Studio':^{WIDTH}}")
print("=" * WIDTH)
print(" 1. Register / replace the task")
print(" 2. View the task card")
print(" 3. Change the priority")
print(" 4. Mark as completed")
print(" 5. Quit")
print("-" * WIDTH)

  1. Validating the option read

The option is a value that comes from outside, so it gets the same treatment as any other: the framework from 02-04 — presence, type, domain — and the insistent loop from 03-02.

OPTIONS = ("1", "2", "3", "4", "5")
option = input("Choose an option (1-5): ").strip()
while option not in OPTIONS:
    option = input("Invalid option. Choose 1-5: ").strip()

Three design decisions worth understanding:

  • We compare text, not numbers. OPTIONS holds "1" and not 1, so no conversion is needed. If the user types hello, there is no ValueError to worry about: it simply is not in the catalogue. Converting with int() without checking first would break the program.
  • The .strip() is essential. A space in front of the number turns " 1" into invalid data, and the user would not see why.
  • Enter with nothing typed produces the empty string, which is not in OPTIONS either, so the loop asks again with no need for any special case. That said: the telling-off message should be clear, because a menu that answers in silence looks like it has hung.

If you also want to accept shortcuts such as q for quit, it is enough to normalise with .lower() and widen the catalogue: OPTIONS = ("1", "2", "3", "4", "5", "q").

  1. Confirmations for destructive actions

An action is destructive if it loses information the user cannot get back: replacing the registered task, deleting it, leaving the application without saving. Faced with those, a polite program asks.

confirm = input("Are you sure you want to replace the task? (y/n): ").strip().lower()
if confirm == "y":
    print("Replacing...")
else:
    print("Operation cancelled.")

The important detail is in the else: we compare against "y" and anything else cancels. That is deliberate. When in doubt — a clumsy finger, an accidental Enter, an unintended x — the safe option is to destroy nothing; if we wrote if confirm != "n":, any wrong key would carry out the irreversible action. And do not overdo it: if the program asks for confirmation about everything, the user learns to type y without reading and the confirmation stops protecting anything. Confirm only what genuinely cannot be undone.

  1. EasyTask v0.6: the complete application

Everything is in place now. EasyTask stops being a script and becomes an application with five options over the one and only task it knows how to manage. Notice a new and decisive variable: has_task, the flag that tells "nothing has been registered yet" apart from "there is a task to look at or change".

# easytask.py - Alba Studio
# Version 0.6: application with a main menu

WIDTH = 46
PRIORITIES = ("high", "medium", "low")
TEAM = ("Marta", "Luis", "Nuria")
OPTIONS = ("1", "2", "3", "4", "5")

# --- Application state: the data of THE task ---
has_task = False
title = ""
assignee = ""
priority = ""
days = 0
completed = False

while True:
    print("\n" + "=" * WIDTH)
    print(f"{'EasyTask v0.6 - Alba Studio':^{WIDTH}}")
    print("=" * WIDTH)
    print(" 1. Register / replace the task")
    print(" 2. View the task card")
    print(" 3. Change the priority")
    print(" 4. Mark as completed")
    print(" 5. Quit")
    print("-" * WIDTH)

    option = input("Choose an option (1-5): ").strip()
    while option not in OPTIONS:
        option = input("Invalid option. Choose 1-5: ").strip()

    if option == "1":
        if has_task:
            confirm = input(f"'{title}' already exists. Replace it? (y/n): ").strip().lower()
            if confirm != "y":
                print("Operation cancelled.")
                continue
        title = input("Title         : ").strip()
        while title == "":
            title = input("Empty. Title  : ").strip()
        assignee = input("Assignee      : ").strip().capitalize()
        while assignee not in TEAM:
            assignee = input("Not on the team. Assignee: ").strip().capitalize()
        priority = input("Priority      : ").strip().lower()
        while priority not in PRIORITIES:
            priority = input("Not valid. Priority: ").strip().lower()
        days_text = input("Days (1-365)  : ").strip()
        while not days_text.isdigit() or not 1 <= int(days_text) <= 365:
            days_text = input("Integer 1-365. Days: ").strip()
        days = int(days_text)
        completed = False
        has_task = True
        print("Task registered successfully.")
    elif option == "2":
        if not has_task:
            print("There is no task registered yet.")
        else:
            status = "Completed" if completed else "Pending"
            print("-" * WIDTH)
            print(f"{'Title':<14}{title:>{WIDTH - 14}}")
            print(f"{'Assignee':<14}{f'{assignee} ({priority})':>{WIDTH - 14}}")
            print(f"{'Days / status':<14}{f'{days} / {status}':>{WIDTH - 14}}")
    elif option == "3":
        if not has_task:
            print("There is no task registered yet.")
        else:
            new_priority = input("New priority (high/medium/low): ").strip().lower()
            while new_priority not in PRIORITIES:
                new_priority = input("Not valid. Priority: ").strip().lower()
            print(f"Priority of '{title}': {priority} -> {new_priority}")
            priority = new_priority
    elif option == "4":
        if not has_task:
            print("There is no task registered yet.")
        elif completed:
            print("The task was already completed.")
        else:
            confirm = input(f"Mark '{title}' as completed? (y/n): ").strip().lower()
            if confirm == "y":
                completed = True
                print("Task completed. Good work.")
            else:
                print("Operation cancelled.")
    elif option == "5":
        confirm = input("Are you sure you want to quit? (y/n): ").strip().lower()
        if confirm == "y":
            print("See you later. EasyTask is closing.")
            break

    input("\nPress Enter to return to the menu...")

Four details of the code deserve a comment:

  • The state lives outside the loop. The variables title, priority, has_task… are declared before the while and survive every round. If they were inside, each round would reset them and the application would forget the task the moment it painted the menu. It is the same principle as the accumulator from 03-02.
  • has_task protects options 2, 3 and 4. Without that check, choosing "View the task card" before registering anything would show an empty, absurd card. A serious application never offers data it does not have.
  • The continue in option 1 cancels the replacement and returns to the menu, skipping the rest of the round, including the final pause. It is exactly the continue from 03-03 applied to the main loop.
  • Quitting is confirmed too, because in the current version closing the program loses the task. And only "y" quits: any other key stays in the application.

A typical working session for Marta:

Choose an option (1-5): 9
Invalid option. Choose 1-5: 1
Title         : Book fair poster
Assignee      : nuria
Priority      : superurgent
Not valid. Priority: high
Days (1-365)  : 4
Task registered successfully.

Press Enter to return to the menu...

After that, option 3 changes the priority to medium, option 2 shows the updated card and option 5 asks for confirmation before closing. The same run of the program does all of that, which is precisely what we could not do in any earlier version.

Common Mistakes and Tips

Declaring the state inside the loop. The number-one menu mistake: the application "forgets" the data on every round. Everything that has to survive goes before the while.

Forgetting the break in the quit option. The menu becomes inescapable and only Ctrl+C is left. Always check this first.

Converting the option with int() without validating. If the user types a letter, ValueError and a dead program. Work with the option as text and compare it against a catalogue.

Not pausing before repainting. The result appears and disappears in the same blink; an input("Press Enter...") sorts it out. And do not offer impossible options, such as looking at a task that does not exist yet: a state flag like has_task and a clear message avoid it.

Confirming with != "n". Any stray key runs the destructive action. Always confirm with == "y" and let everything else cancel.

Tip: keep the number of options in a constant. With OPTIONS at the top of the file, adding an option means touching two places (the constant and the menu), not rummaging through the whole program.

Tip: test your menu like a hostile user. Type letters, spaces, a bare Enter, numbers out of range, and choose option 2 right after starting up. Everything that breaks the program is a missing validation.

Exercises

Exercise 1: Spot the faults

This menu has three serious defects. Find them and explain what happens to the user in each case.

while True:
    use_count = 0
    print("1. View  2. Quit")
    option = int(input("Option: "))
    if option == 1:
        use_count += 1
        print(f"Views: {use_count}")
    elif option == 2:
        print("Goodbye")

Exercise 2: Team diary menu

Write team_agenda.py, an application with a main loop and a four-option menu that manages a single diary note: 1) write/replace the note (non-empty text and a weekday between 1 and 5), 2) view the note, 3) delete the note (with confirmation), 4) quit. Use a has_note flag, validate the option with a while, dispatch with match and pause before repainting.

Exercise 3: Operation counter

Extend EasyTask v0.6 so that it keeps track of how many times each option has been used during the session and shows a summary on quitting, using the counter pattern from 03-02. Say where the counters should be declared and why.

Solutions

Solution 1.

Defect What happens to the user
use_count = 0 is inside the loop It resets on every round: the counter always shows 1
int(input(...)) without validation If they type a letter or press Enter, the program dies with ValueError
Option 2 has no break It prints "Goodbye" and returns to the menu: quitting is impossible

All three have the same root: the loop was written thinking only of the user who does the expected thing. The corrected version takes the counter outside the while, reads the option as text validated against a catalogue and adds the break.

Solution 2.

# team_agenda.py - Alba Studio
OPTIONS = ("1", "2", "3", "4")
DAYS = ("1", "2", "3", "4", "5")
has_note = False
note = ""
day = ""

while True:
    print(" 1. Write / replace the note        2. View the note")
    print(" 3. Delete the note                 4. Quit")
    option = input("Choose an option (1-4): ").strip()
    while option not in OPTIONS:
        option = input("Invalid option. Choose 1-4: ").strip()

    match option:
        case "1":
            note = input("Text of the note: ").strip()
            while note == "":
                note = input("It cannot be empty. Note: ").strip()
            day = input("Working day (1-5): ").strip()
            while day not in DAYS:
                day = input("It must be 1 to 5. Day: ").strip()
            has_note = True
            print("Note saved.")
        case "2":
            if has_note:
                print(f"Day {day}: {note}")
            else:
                print("There is no note written.")
        case "3":
            if not has_note:
                print("There is nothing to delete.")
            else:
                confirm = input(f"Delete '{note}'? (y/n): ").strip().lower()
                if confirm == "y":
                    has_note = False
                    note = ""
                    print("Note deleted.")
                else:
                    print("Operation cancelled.")
        case "4":
            print("See you later.")
            break

    input("\nPress Enter to continue...")

Notice that the break in case "4" works perfectly normally: it belongs to the while, not to the match. And that deleting is not just a matter of setting has_note = False: it is worth clearing note as well, so that no path through the program can show data the user believes is gone.

Solution 3.

The counters — one per option — must be declared before the while, alongside the rest of the state, for exactly the same reason as has_task: if they were inside, they would reset on every round and would always be 1. They are incremented at the top of each option's block, and the summary is printed just before the break:

uses_register = 0
uses_view = 0
uses_priority = 0
uses_complete = 0

while True:
    # ... menu and option validation ...
    if option == "1":
        uses_register += 1
        # ... registering the task ...
    elif option == "5":
        confirm = input("Are you sure you want to quit? (y/n): ").strip().lower()
        if confirm == "y":
            print(f"Summary: {uses_register} registrations, {uses_view} views,")
            print(f"{uses_priority} priority changes, {uses_complete} completions.")
            break

And here a problem appears that is no longer about loops: four almost identical variables to count four almost identical things. It is the same symptom we will see in the conclusion, and its solution is in module 5.

Module conclusion

EasyTask has reached version 0.6 and is now a real application. It shows a menu, waits for orders, validates what it receives, runs the requested action, warns when something cannot be done, confirms whatever is irreversible and only closes when the user says so. Marta can register the task, look at it, change its priority and mark it as completed without relaunching the program.

And with that, module 3 closes. You know how to decide with if, elif and else, with compound conditions, nesting and guard clauses (03-01). You know how to repeat with while and for, with range() and its three forms, and with the four classic patterns — counter, accumulator, maximum and flag — (03-02). You know how to control the flow precisely with break, continue, the else clause of loops, nested loops and match (03-03). And you know how to assemble all of that into an application loop with a menu, validation, dispatching, pause and confirmations (03-04). With these three structures — sequence, decision and repetition — you can express any algorithm that exists: it is a classic result in computer science, and you already have the whole of it.

But look at version 0.6 with a critical eye, because its two limits are the map of what is coming. The first: the code has become long and repetitive. Almost a hundred lines in a single block, with if not has_task: written three times, four validation loops as alike as two peas in a pod, and a menu that will grow every time we add a feature. All of that is crying out for the ability to give a name to a piece of code and reuse it: to write ask_priority() once and call it from wherever it is needed. That is what functions are, and they are the whole of module 4.

The second: there is only room for one task. The entire application revolves around a single set of variables — title, assignee, priority, days — and registering a new task means destroying the previous one. That is why option 1 asks for confirmation. But the studio does not have one task: it has dozens, and Marta needs to see them all, filter them by assignee and sort them by priority. For that we need structures capable of holding many values under a single name: lists, which arrive in Lists and arrays, and with them the for that walks through a real collection of tasks.

Put another way: you know how to build a program's machinery, and now it is time to learn to organise it and to give it data worth working with. See you in module 4, with functions.

© Copyright 2026. All rights reserved