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
- Anatomy of the main loop
while Truewithbreak, and the flag variant- Dispatching the chosen option
- Designing a menu that makes sense
- Validating the option read
- Confirmations for destructive actions
- EasyTask v0.6: the complete application
- Common mistakes and tips
- Exercises
- Module conclusion
- 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.
while True with break, and the flag variant
while True with break, and the flag variantThe 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.")
breakThere 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.
- 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.
- 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 thanOption:.
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)
- 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.
OPTIONSholds"1"and not1, so no conversion is needed. If the user typeshello, there is noValueErrorto worry about: it simply is not in the catalogue. Converting withint()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
OPTIONSeither, 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").
- 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.
- 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 thewhileand 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_taskprotects 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
continuein option 1 cancels the replacement and returns to the menu, skipping the rest of the round, including the final pause. It is exactly thecontinuefrom 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.")
breakAnd 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.
Fundamentals of Programming
Module 1: Introduction to Programming
- What is programming?
- History of programming
- Programming languages
- Development environments
- From problem to algorithm
Module 2: Core Concepts
- Variables and data types
- Operators and expressions
- Input and output
- Type conversion and data validation
Module 3: Control Structures
Module 4: Functions and Procedures
- Defining and using functions
- Parameters and return values
- Variable scope
- Breaking a program down into functions
- Functions as values: lambda and higher order
Module 5: Data Structures
- Lists and arrays
- Strings
- Dictionaries and sets
- Tuples and nested structures
- Saving data to files: text, CSV and JSON
Module 6: Basic Algorithms
Module 7: Objects and Code Organisation
- From data to objects: classes and instances
- Attributes, methods and the constructor
- Collections of objects
- Modules, packages and imports
Module 8: Good Practices and Tools
- Documentation and comments
- Debugging and error handling
- Version control
- Automated testing
- Style, readability and refactoring
