Embercode
✕
LoopsLesson 12 of 16

40 min read

Input-driven menus in Python

Why this matters

Many programs wait for the user to type a command. A note app waits for "add" or "list". A shell waits for a command name. A game waits for "north" or "take lamp". The program does one thing, then asks again, until the user says it is finished.

This pattern is called a command loop, or a menu. It is a loop whose body reads one choice, works out what the choice means, does that work, and goes round again. The loop ends when the user types a quit command, or when the input runs out.

This lesson builds a small notes menu step by step. Along the way it covers three common mistakes: how to stop a loop when the user quits, how to stop when there is no more input, and how to turn typed text into numbers. The loops themselves are while loops, which the earlier lessons of this module cover.

The idea: read, act, repeat

Every command loop has the same three steps:

  1. Show a prompt, which is a short line that tells the user what to type.
  2. Read one line of input and decide what it means.
  3. Do the action for that choice. Then go back to step 1, unless the choice was quit.

Picture a vending machine. It shows its buttons, waits for a press, gives you what you chose, and shows its buttons again. It does not finish after one purchase, and it does not give you anything for a button it does not have. A menu loop works the same way.

The loop needs two exits. One is the quit command, which the user chooses. The other is the end of the input, which happens when there is nothing more to read. A program that handles only the first exit will crash when the input is piped from a file and runs out. The later sections of this lesson deal with that.

In Python, the function input does the reading. It shows the prompt text you give it, waits for the user to press Enter, and returns the line as text without the newline at the end. It always returns text, even when the user types digits.

Worked example 1: a menu with three choices

Start with the smallest useful menu. It keeps a list of notes in memory and has three commands: a adds a note, l lists the notes, and q quits.

notes = []
while True:
    print("Menu: [a]dd note, [l]ist notes, [q]uit")
    try:
        line = input("> ")
    except EOFError:
        print()
        print("No more input. Exiting.")
        break
    choice = line.strip().lower()
    if choice == "q":
        print(f"Bye. Notes saved this session: {len(notes)}")
        break
    elif choice == "a":
        note = input("Note text: ")
        notes.append(note)
    elif choice == "l":
        for i, note in enumerate(notes, start=1):
            print(f"{i}. {note}")
    else:
        print(f"Unknown choice '{choice}'. Try a, l or q.")

Four parts matter here.

The notes list is created once, before the loop. It keeps the notes in the computer's memory while the program runs, so it survives from one pass to the next. A list created inside the loop would start empty on every pass.

The while True loop has no condition of its own. It runs until a break statement leaves it. This is a common shape for a menu, because the decision to stop depends on what the user types, and the check happens inside the body.

The line line.strip().lower() removes spaces at the start and end, and converts the text to lower case. So "Q", " q " and "q" all mean the same command. A user should not get an error for typing a capital letter.

The try and except EOFError lines handle the end of input. The mistakes section explains them.

Tracing one run

Here is the program run with this input, one line for each thing the user types: a, buy milk, l, zz, q.

The run was made by piping the input in from a command, so the typed lines do not appear on the screen. In a terminal, each line you type would start on its own row.

Menu: [a]dd note, [l]ist notes, [q]uit
> Note text: Menu: [a]dd note, [l]ist notes, [q]uit
> 1. buy milk
Menu: [a]dd note, [l]ist notes, [q]uit
> Unknown choice 'zz'. Try a, l or q.
Menu: [a]dd note, [l]ist notes, [q]uit
> Bye. Notes saved this session: 1

Read the trace in order. The first pass reads a, so the program asks for the note text and reads buy milk. The second pass reads l and prints the list. The third pass reads zz, which is not a command, so it prints a message and asks again. The fourth pass reads q, prints the goodbye and leaves the loop.

The unknown command does not stop the program. The user can try again. That is the main reason for a loop: a wrong choice costs one line, not the whole session.

The goodbye message says "this session" because the notes are lost when the program ends. A menu that must keep its notes after the program ends would write them to a file or a database, which is outside this lesson.

Worked example 2: exiting cleanly

The q branch uses break. The break statement leaves the nearest loop, which here is the while True loop. After the loop ends, the code that follows runs. A program can put clean-up code there, such as saving a file or printing a summary.

Some menus put the exit in the loop condition instead. That form suits a menu with a flag, which is a variable that is True while the menu should keep running:

running = True
while running:
    choice = input("> ").strip().lower()
    if choice == "q":
        running = False
    # the rest of the choices run here

The flag form makes the loop condition easy to read: "keep running while running is True". The cost is that the loop does not stop until the body finishes the current pass. With break, the loop stops at once. Both forms are correct. Pick the one that reads more clearly to someone who opens the file later.

Choosing between if and match

Python has two ways to pick a branch from a menu. One is an if chain with elif. The other is match, which was added in Python 3.10. A match statement compares the value with a list of patterns, and runs the first one that fits. The case _: line at the end is the default, which runs when nothing else fits.

The table compares them for a menu.

FormGood forWatch out for
if / elifCommands with conditions, such as "if the list is empty"Long chains are hard to scan
match / caseA fixed list of exact words, such as a, l, qNeeds Python 3.10 or newer

The examples in this lesson use if chains, because later checks, such as the list size, are conditions, not exact matches. A match statement is a good fit when every branch compares the whole command word with one fixed value.

Here is a menu written with match. It has a break in the quit case:

while True:
    line = input()
    match line:
        case "q":
            print("quitting")
            break
        case _:
            print(f"got {line}")
print("after loop")

Input: q

Output:

quitting
after loop

The break leaves the while loop, so the code after the loop runs. This is the same result that a flag gives, and no flag is needed. In Java, a break inside a switch leaves only the switch, and a flag is the usual fix. Python's match does not have that trap.

Worked example 3: commands with an argument

Many commands need a value after the command name. "d 2" means "delete note 2". The menu must split the line into two parts: the command word and the rest of the line.

The method split(maxsplit=1) does this. It splits the text at the first run of spaces, and stops after two parts. Called with no separator, split also ignores spaces at the start and end of the line. So "d 2" becomes the parts d and 2, and "a call Ana" becomes a and call Ana.

print("d 2".split(maxsplit=1))
print("a call  Ana".split(maxsplit=1))
print("l".split(maxsplit=1))
print("".split(maxsplit=1))

Output:

['d', '2']
['a', 'call  Ana']
['l']
[]

The second part keeps its own spaces, which matters for note text. The last line shows an empty line. split returns an empty list for it, not a list with one empty string. The mistakes section shows what goes wrong without a check for that case.

The full program keeps the loop from the first example and replaces its command reading with the split line and these branches:

notes = []
while True:
    try:
        line = input("> ")
    except EOFError:
        print()
        print("No more input. Exiting.")
        break
    parts = line.strip().split(maxsplit=1)
    if not parts:
        continue
    command = parts[0].lower()
    rest = parts[1] if len(parts) > 1 else ""
    if command == "q":
        print("Bye.")
        break
    elif command == "a":
        notes.append(rest)
        print(f"Added note {len(notes)}.")
    elif command == "l":
        for i, note in enumerate(notes, start=1):
            print(f"{i}. {note}")
    elif command == "d":
        try:
            number = int(rest)
        except ValueError:
            print("Give a note number, for example: d 2")
            continue
        if number < 1 or number > len(notes):
            print(f"No note {number}. You have {len(notes)}.")
            continue
        removed = notes.pop(number - 1)
        print(f"Deleted: {removed}")
    else:
        print(f"Unknown command '{command}'.")

The rest variable is an empty string when the user types only the command word, such as l or q. The line parts[1] if len(parts) > 1 else "" checks for that. Without the check, parts[1] would fail for those lines with an IndexError.

The delete branch reads the number from rest. It uses a try block to catch a value that is not a number, and it checks the range before it changes the list. The two continue statements skip the rest of the body and go back to the prompt. Without continue, the code below would run anyway, with number missing or out of range. The pop method removes the item at an index and returns it. The expression number - 1 converts the menu number, which starts at 1, to the list index, which starts at 0.

Run with the input a buy milk, a call Ana, l, d 1, d x, d 9, l, zz, q:

> Added note 1.
> Added note 2.
> 1. buy milk
2. call Ana
> Deleted: buy milk
> Give a note number, for example: d 2
> No note 9. You have 1.
> 1. call Ana
> Unknown command 'zz'.
> Bye.

The output shows each path of the menu. Two of the inputs were wrong, and the program handled both by printing a message and asking again. The list was checked after each change, so the note numbers shown by l always match the numbers that d accepts.

Handling the end of input

The first example calls input inside a try block. When the input has no more lines, input raises an error called EOFError, which stands for end of file. The except EOFError branch catches it and leaves the loop. The check matters for two situations. A program fed from a file runs out of lines at the end of the file. A user at a terminal can press Ctrl+D on Linux or macOS, or Ctrl+Z and then Enter on Windows, to tell the program the input is finished.

Without the check, the program crashes. The first example has a try block only around the command read. Its note branch has no check, so it shows what happens when the input ends while the program waits for the note text. Run the first example with the input a and then the end of the input:

Menu: [a]dd note, [l]ist notes, [q]uit
> Note text: Traceback (most recent call last):
  File "menu.py", line 15, in <module>
    note = input("Note text: ")
EOFError: EOF when reading a line

The first line of the traceback names the file where the error happened. The path depends on where the file is saved, so it may differ on your computer. The menu read the command a, asked for the note text, and found no input. The try block around the command read did not help, because it only protects that read. Every input call needs its own check, or needs a plan for the case where it has no line to read.

The fixed branch looks like this:

elif choice == "a":
    try:
        note = input("Note text: ")
    except EOFError:
        break
    notes.append(note)

With the fix, the program leaves the loop at the note text. A message can be printed before the break, if the user needs to know why the program stopped.

Reading numbers in a menu

A menu often asks for a number, such as a menu choice "1" to "3", or an amount to add. In Python the rule is the same as for every other command: read the line as text, then turn the text into a number with int. The int function raises a ValueError when the text is not a whole number, so the conversion goes inside a try block:

while True:
    text = input("> ").strip()
    try:
        value = int(text)
    except ValueError:
        print(f"'{text}' is not a whole number.")
        continue
    print(f"value {value}")
    break

Input: x, then 4:

> 'x' is not a whole number.
> value 4

The continue statement jumps to the next pass of the loop, so the program asks again. The text x becomes a message, not a crash.

Some languages have a number reader that reads a number from the middle of a line and leaves the end of the line behind. That can make the next line read come back empty. Python's input always reads a whole line, so this problem does not come up.

If the int call is left out, the value stays as text, and a comparison with a number fails. The mistakes section shows the message.

Common mistakes

Calling input without checking for the end of input

As shown above, a read with no line left raises EOFError with the message EOF when reading a line. The fix is to wrap each input call in a try block with except EOFError, or to put all the reads in one place that does the check.

Comparing text with is instead of ==

The keyword is checks whether two names point to the same object in memory. It does not check whether the text is the same. Python warns about this when the literal is written in the code:

choice = input()
if choice is "quit":
    print("matched with is")
else:
    print("did not match with is")
if choice == "quit":
    print("matched with ==")

Input: quit

Output:

did not match with is
matched with ==

The warning that Python prints first begins with SyntaxWarning and says "is" with 'str' literal. Did you mean "=="?. Always compare text with ==.

Forgetting strip and lower

If the user types q with a space, or Q, a check for "q" fails and the program says the choice is unknown. Calling strip() and lower() on the line removes both problems in one step.

Comparing text with a number

input returns text. A comparison such as number < 3 between text and a number is an error:

number = input("Pick: ")
if number < 3:
    print("small")

Input: 5

The last line of the error message is:

TypeError: '<' not supported between instances of 'str' and 'int'

The name str means text. The fix is to convert the text first, with number = int(input("Pick: ")).

Calling split on an empty line and reading index 0

split returns an empty list for an empty line. Reading parts[0] from that list fails:

parts = "".split(maxsplit=1)
print(parts[0])

Output:

IndexError: list index out of range

The fix is the check if not parts: continue, which is in the program of the third example.

Practice

Exercise 1. Add a command h to the notes menu that prints a short list of the commands. Where does the new branch go, and does it need a break?

Show the answer

Add a new elif branch next to the others:

elif command == "h":
    print("a adds a note, l lists them, q quits.")

It does not need a break. The if chain runs only the matching branch, and then the loop goes round again.

Exercise 2. Add a command c that clears all the notes and prints "Notes cleared." Why is the list notes created outside the loop?

Show the answer

elif command == "c":
    notes.clear()
    print("Notes cleared.")

The list is created outside the loop so it survives from one pass to the next. If it were created inside the loop, each pass would start with an empty list and the notes would be lost.

Exercise 3. The notes menu exits with break when the input ends. Change the program so it prints the number of notes when it exits, whether the user typed q or the input ran out.

Show the answer

Move the summary line below the loop, so both exits reach it:

notes = []
while True:
    ...  # the menu code from the first example goes here
print(f"Notes saved this session: {len(notes)}")

Both break statements leave the loop, and the line after the loop runs in either case. The ... line is a placeholder that does nothing. In the real program, the menu code takes its place.

Exercise 4. Read a menu choice that should be a number from 1 to 3. Use input and int. When the input is not a number or is out of range, print a message and ask again. When the input ends, stop asking. Show the code.

Show the answer

choice = 0
while choice not in (1, 2, 3):
    try:
        text = input("Pick 1, 2 or 3: ")
    except EOFError:
        break
    try:
        choice = int(text)
    except ValueError:
        choice = 0
    if choice not in (1, 2, 3):
        print("Please type 1, 2 or 3.")

Input x, then 5, then 2 gives two messages and then the value 2. If the input ends early, choice is 0 or a number outside 1 to 3, so the code after the loop must check for that.

Exercise 5. Explain why this loop stops after the user types q, even though it has no flag. Then say what would have happened in Java if the same code had been written with a switch and no flag.

while True:
    line = input()
    match line:
        case "q":
            print("quitting")
            break
        case _:
            print(f"got {line}")
print("after loop")

Show the answer

The break leaves the while loop, not the match. The code after the loop, print("after loop"), runs next. In Java, a break inside a switch leaves only the switch. The loop would then ask for another line, and the program would crash when the input ran out. That is why Java menus often use a flag.

Summary

  • A command loop reads a choice, runs the matching action, and repeats until the user quits.
  • Create the list of notes once, before the loop. Strip and lower-case each line before comparing it.
  • Use break to leave the loop when the user quits. Inside a match statement, break still leaves the loop.
  • Wrap each input call in try with except EOFError, so the program exits cleanly when the input ends.
  • Read numbers as text with input and int, inside a try block, so bad input is a message, not a crash.
Check yourself: Digits and menus in Python (7 questions) →Practice this topic: coding problems for Loops →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Digit loops in Python