Embercode
✕
Flowcharts and PseudocodeLesson 8 of 13

40 min read

Pseudocode for input validation

Why this matters

Every program that asks a person for something has to deal with wrong answers. A person types abc when you asked for an age. Someone types 150 for a month of the year. Someone presses Enter and sends nothing at all. A program that does not check these cases will either crash or quietly store nonsense, and the nonsense is harder to find later.

Input validation means checking a value before the program uses it. Validation has three jobs: decide whether the value has the right kind (a whole number, a yes or a no), decide whether it is in the allowed range (0 to 120, 1 to 12), and decide what to do when it fails (usually: show a message and ask again).

Each of those jobs is a small piece of logic: a question, a branch and a loop. This makes validation a good topic for pseudocode. The steps are short, the order matters, and a mistake in the order causes a bug that only shows up when someone types the wrong thing.

This lesson builds on the conventions from the pseudocode conventions lesson, especially WHILE, IF and RETURN. It does not repeat how to draw these steps as a flowchart. The flowchart lesson for loops and early exits covers the picture form of the same ideas.

The idea: check, complain, ask again

Think about a shop counter. The clerk asks, "How many items?" You say "forty". The clerk does not sell you forty items of nothing. The clerk says, "Sorry, I need a number," and asks again. The clerk keeps asking until the answer is something the shop can use, and only then starts the sale.

Input validation works the same way. The program has a re-prompt loop: a loop that asks the question, checks the answer, and asks again if the answer is not acceptable. The loop only ends when the answer passes every check. Only then does the rest of the program run.

A re-prompt loop is a loop whose body is "ask, then check". The word prompt means the question shown to the user. To re-prompt is to ask the same question again.

Here is the whole shape with no details. Read it once, then we will fill in each part:

1  WHILE TRUE
2      READ answer
3      IF answer is acceptable THEN
4          RETURN answer
5      END IF
6      PRINT an error message that says what was wrong
7  END WHILE

Line 1 starts a loop that runs until something inside it exits. TRUE is never false, so the loop only stops because of line 4, which is a RETURN. A RETURN leaves the function at once, so the loop stops too. This is the early exit: the loop does not need a separate flag to stop.

Line 2 reads what the user typed. Line 3 asks the question "is this acceptable?", and that question is the whole validation. Line 6 runs only when the answer was not acceptable. After line 6, the loop goes back to line 1 and asks again.

Notice what the pseudocode does not say. It does not say what "acceptable" means. Lines 3 and 6 are still vague, and the rest of this lesson makes them exact.

Rule 2 applied: say how, not just what

The pseudocode conventions lesson gave a test for each line: could a literal-minded helper carry it out without further thought? "Check the answer" fails that test. A helper would ask: check it how? "Answer is a number" passes some of the test, but does not say what counts as a number. A minus sign? A decimal point? A letter that looks like a digit?

Good validation pseudocode says the check in terms a helper can carry out. For an age, the steps are:

  1. The text must not be empty.
  2. Every character in the text must be one of 0 to 9.
  3. Convert the text to a number.
  4. The number must be 0 or more and 120 or less.

Each step is one question with a yes or no answer. Writing them as numbered lines makes the order visible, and the order matters. You cannot check the range before you have a number, so step 3 comes before step 4.

The second step is the one most beginners write too loosely. "Text is a number" is a vague claim. "Every character is 0 to 9" is exact. Later in this lesson you will see why the exact version matters: the vague version can pass a check and then crash the conversion.

Worked example 1: one age, read until it is valid

The first example asks for an age and accepts only whole numbers from 0 to 120. Here is the pseudocode, written as numbered steps. Each step is one action or one question.

1   FUNCTION read_age()
2       WHILE TRUE
3           READ text
4           IF text is empty OR text has a character that is not 0 to 9 THEN
5               PRINT "Error: type a whole number with no sign."
6               NEXT PASS OF THE LOOP
7           END IF
8           age <- text converted to a whole number
9           IF age > 120 THEN
10              PRINT "Error: age must be 120 or less."
11              NEXT PASS OF THE LOOP
12          END IF
13          RETURN age
14      END WHILE
15  END FUNCTION

NEXT PASS OF THE LOOP means "skip the rest of this pass through the loop and go back to the top". Some languages call this continue. The word is not in the conventions table, so the lesson defines it here and uses it only in this sense.

Read the pseudocode with a finger, one line at a time. Pretend the user typed abc. Line 3 reads abc. Line 4 asks whether it is empty or has a character outside 0 to 9. The answer is yes, because a is not a digit. Lines 5 and 6 print the error and go back to the top. The user is asked again.

Now pretend the user typed 42. Line 4 is no, because every character is a digit. Line 8 converts the text to 42. Line 9 asks whether 42 is more than 120. It is not, so the IF body is skipped. Line 13 returns 42, which ends the function.

Here is the same logic in Python. The translation is almost line for line:

def read_age():
    while True:
        text = input("Enter your age (0 to 120): ")
        if text == "" or not text.isdecimal():
            print("Error: type a whole number with no sign.")
            continue
        age = int(text)
        if age > 120:
            print("Error: age must be 120 or less.")
            continue
        return age
 
age = read_age()
print("Age accepted:", age)

Two translation notes. input in Python reads one line and returns it as text, with the Enter key removed. isdecimal is a method of text that returns True only when every character is a decimal digit. continue in Python is the word for NEXT PASS OF THE LOOP.

The line text == "" or not text.isdecimal() covers step 1 and step 2 in one test. The empty string is not decimal, so not text.isdecimal() is already true for it. The explicit test text == "" is still useful because it makes the intent visible to a reader. Pseudocode should say what it means, even when the language would catch the case anyway.

Save the Python above as age_check.py, then run it. The input is piped in, so each line plays the part of something the user typed. The first line is abc, the second is -5, the third is 2² (a 2 followed by a superscript two), the fourth is 150, and the fifth is 42:

printf 'abc\n-5\n2\xc2\xb2\n150\n42\n' | python3 age_check.py
Enter your age (0 to 120): Error: type a whole number with no sign.
Enter your age (0 to 120): Error: type a whole number with no sign.
Enter your age (0 to 120): Error: type a whole number with no sign.
Enter your age (0 to 120): Error: age must be 120 or less.
Enter your age (0 to 120): Age accepted: 42

The superscript case is worth a moment. A character like ² looks like a digit to a person, and Python's isdigit method returns True for it. The conversion int("2²") then fails. Using isdecimal avoids that trap, which is why the pseudocode says "0 to 9" and the Python uses isdecimal. The Mistakes section shows the crash that happens with isdigit.

Worked example 2: a reusable range check

The first example works for one question. Most programs ask many questions with different limits: a menu choice from 1 to 3, a day from 1 to 31, a score from 0 to 100. Writing the loop each time is slow and invites copy mistakes. A better approach is one function that takes the question and the limits as parameters. A parameter is a value passed into a function when it is called, so the same function can serve different questions.

1   FUNCTION ask_in_range(prompt, low, high)
2       WHILE TRUE
3           READ text, showing prompt first
4           IF text is empty OR text has a character that is not 0 to 9 THEN
5               PRINT "Error: type a whole number."
6               NEXT PASS OF THE LOOP
7           END IF
8           value <- text converted to a whole number
9           IF value < low OR value > high THEN
10              PRINT "Error: type a number from " + low + " to " + high + "."
11              NEXT PASS OF THE LOOP
12          END IF
13          RETURN value
14      END WHILE
15  END FUNCTION

Two things changed from example 1. The limits are no longer fixed numbers, so the check on line 9 uses the parameters low and high. The check also now has two parts joined by OR. A value is rejected if it is below low or above high. Writing the two comparisons with OR is clearer than writing "not between", because not between is easy to misread.

The pseudocode on line 10 joins text and numbers with +. This is a small shortcut. Some languages need you to convert the numbers to text first, and the Python below does that for you with an f-string.

The yes or no question is a second validator with a different shape. It does not read a number. It reads a word and accepts only y or n:

1   FUNCTION ask_yes_no(prompt)
2       WHILE TRUE
3           READ text, showing prompt first
4           text <- text with spaces removed and lowercase letters
5           IF text == "y" THEN
6               RETURN TRUE
7           END IF
8           IF text == "n" THEN
9               RETURN FALSE
10          END IF
11          PRINT "Error: type y or n."
12      END WHILE
13  END FUNCTION

Line 4 is a normalising step. To normalise is to change the input into one standard form before comparing it. Without this line, Y and y would be rejected, and most people would find that annoying. Normalising is a choice you make on purpose, so it belongs in the pseudocode, where a reader can see it.

Here is the Python for both functions and a short main part that uses them:

def ask_in_range(prompt, low, high):
    while True:
        text = input(prompt)
        if text == "" or not text.isdecimal():
            print("Error: type a whole number.")
            continue
        value = int(text)
        if value < low or value > high:
            print(f"Error: type a number from {low} to {high}.")
            continue
        return value
 
def ask_yes_no(prompt):
    while True:
        text = input(prompt).strip().lower()
        if text == "y":
            return True
        if text == "n":
            return False
        print("Error: type y or n.")
 
choice = ask_in_range("Pick a menu item (1-3): ", 1, 3)
print("You picked", choice)
sure = ask_yes_no("Are you sure? (y/n): ")
print("Confirmed" if sure else "Cancelled")

Save the code as ask_in_range.py and run it with a first input of 0 (out of range), then x (not a number), then 2 (valid), then maybe (not y or n), then Y (valid after normalising):

printf '0\nx\n2\nmaybe\nY\n' | python3 ask_in_range.py
Pick a menu item (1-3): Error: type a number from 1 to 3.
Pick a menu item (1-3): Error: type a whole number.
Pick a menu item (1-3): You picked 2
Are you sure? (y/n): Error: type y or n.
Are you sure? (y/n): Confirmed

Each error message matches the branch that produced it. The first error comes from the range check, the second from the digit check. Keeping the two checks as separate IF branches, each with its own message, is what lets the user know exactly what went wrong.

Run it once more. The first answer, 4, is out of range. The second, 3, is valid. The last answer, n, takes the cancel branch:

printf '4\n3\nn\n' | python3 ask_in_range.py
Pick a menu item (1-3): Error: type a number from 1 to 3.
Pick a menu item (1-3): You picked 3
Are you sure? (y/n): Cancelled

The 4 was rejected by the range check, the 3 was accepted, and n gave the cancel branch.

Worked example 3: validating several fields in order

The third example is a small sign-up form with three fields: an age, a day of the month, and an agreement to terms. The rule is that the program must not move on to the next field until the current one is valid. This is a pattern called sequential validation. Each field is checked on its own, one after another, using the functions from example 2.

1   FUNCTION sign_up()
2       age <- ask_in_range("Age (0 to 120): ", 0, 120)
3       day <- ask_in_range("Day of month (1 to 31): ", 1, 31)
4       agreed <- ask_yes_no("Do you agree to the terms? (y/n): ")
5       IF agreed == FALSE THEN
6           PRINT "Sign-up cancelled. You did not agree."
7           RETURN FALSE
8       END IF
9       PRINT "Saved: age " + age + ", day " + day
10      RETURN TRUE
11  END FUNCTION

The steps are simple because the hard part lives in the helper functions. The point of this example is the order and the early exit. Line 5 checks the answer to the last question before doing anything with the earlier answers. If the user says no, the function prints a message and returns without saving anything. A reader can see at a glance that nothing is saved on the no path.

Here is the Python, with the functions from example 2 above it:

def sign_up():
    age = ask_in_range("Age (0 to 120): ", 0, 120)
    day = ask_in_range("Day of month (1 to 31): ", 1, 31)
    agreed = ask_yes_no("Do you agree to the terms? (y/n): ")
    if agreed is False:
        print("Sign-up cancelled. You did not agree.")
        return False
    print(f"Saved: age {age}, day {day}")
    return True
 
sign_up()

Save the code as signup.py and run it with an age of 200 (rejected), then 30, then a day of 0 (rejected), then 12, then y:

printf '200\n30\n0\n12\ny\n' | python3 signup.py
Age (0 to 120): Error: type a number from 0 to 120.
Age (0 to 120): Day of month (1 to 31): Error: type a number from 1 to 31.
Day of month (1 to 31): Do you agree to the terms? (y/n): Saved: age 30, day 12

The output looks confusing at first because the prompts and the messages run together on one line. The typed answers come from a pipe, so they are not shown on screen, and no line break appears after each prompt. The order is still correct: the age was rejected once, then accepted, then the day was rejected once, then accepted, then the agreement was given.

Common mistakes

Mistake 1: forgetting the loop, so the check runs once. If the pseudocode has IF where it should have WHILE, the program asks once, checks once and moves on whether the answer was good or bad. A bad answer then reaches the next line of the program. In Python this can show up as a crash later, far away from the cause. The fix is to make sure the check is inside a loop that repeats until a RETURN happens.

Mistake 2: a loop that never exits, or that never reads new input. This one is easy to miss. Suppose the loop body prints the error but does not call READ again, or does not move on to the next pass. The loop then repeats the same bad value forever, and the program appears frozen, printing the same message over and over. In pseudocode, check that every path through the loop either returns or reads a new value.

Mistake 3: converting before checking. Calling int(text) before checking the text gives a crash on bad input. This is a common crash in Python. The real error looks like this:

ValueError: invalid literal for int() with base 10: 'abc'

The message is accurate. int was given text it cannot turn into a whole number. The fix is to put the check on lines 4 to 7 of the pseudocode before the conversion on line 8. Validate first, convert second.

Mistake 4: a check that passes a character the conversion rejects. isdigit returns True for some characters that int cannot read, such as the superscript two. The sequence below shows the problem. The first line of output is the isdigit result and the second is the crash:

text = "2²"
print("isdigit:", text.isdigit())
print(int(text))
isdigit: True
Traceback (most recent call last):
  File "mistake_isdigit.py", line 3, in <module>
    print(int(text))
          ~~~^^^^^^
ValueError: invalid literal for int() with base 10: '2²'

The validation said the text was fine, and the conversion disagreed. The fix is to use isdecimal, whose answer matches what int accepts. This is the reason step 2 in the pseudocode says "0 to 9" in exact words: a vague check can disagree with the next step.

Mistake 5: forgetting that input can end. Reading input in a loop assumes that more input will arrive. When a program runs with input from a file or a pipe, the input can run out. Python then raises an error when the loop asks for another line. Running the age program with a single blank line shows it:

printf '\n' | python3 age_check.py
Enter your age (0 to 120): Error: type a whole number with no sign.
Enter your age (0 to 120): Traceback (most recent call last):
  File "age_check.py", line 13, in <module>
    age = read_age()
  File "age_check.py", line 3, in read_age
    text = input("Enter your age (0 to 120): ")
EOFError: EOF when reading a line

The EOFError means "end of file". A program that runs unattended should catch this error and stop cleanly. For this course, the point is that a re-prompt loop has no built-in way to know that no more answers will come. Real programs add that case to the pseudocode as its own branch.

Mistake 6: comparing text to a number. If text holds "42", the range check can go wrong in two ways. The first way is to compare the text with the number 120. Python refuses and stops with an error. The program below shows both comparisons:

text = "42"
print("text > \"120\":", text > "120")
print(text > 120)
text > "120": True
Traceback (most recent call last):
  File "mistake_compare.py", line 3, in <module>
    print(text > 120)
          ^^^^^^^^^^
TypeError: '>' not supported between instances of 'str' and 'int'

The second way is to compare the text with the text "120". Python does not stop, but the answer is wrong. Text is compared one character at a time, and "4" comes after "1", so "42" counts as greater than "120". A range check written this way rejects 42 and reports it as too large, with no error message about the comparison itself. Always convert the text to a number on the line before the range check, as step 8 does in example 1.

Practice

Exercise 1. Write numbered pseudocode for a function read_month() that asks for a month number from 1 to 12, re-prompts until the answer is valid, and returns it. Use the same three checks as example 2: empty or non-digit, conversion, and range.

Show the answer

1   FUNCTION read_month()
2       WHILE TRUE
3           READ text
4           IF text is empty OR text has a character that is not 0 to 9 THEN
5               PRINT "Error: type a whole number."
6               NEXT PASS OF THE LOOP
7           END IF
8           month <- text converted to a whole number
9           IF month < 1 OR month > 12 THEN
10              PRINT "Error: type a number from 1 to 12."
11              NEXT PASS OF THE LOOP
12          END IF
13          RETURN month
14      END WHILE
15  END FUNCTION

Exercise 2. The pseudocode in example 1 checks the empty text and the non-digits in one IF. Rewrite it as two separate IF branches with two different messages. Explain why the separate messages help the user.

Show the answer

Split line 4 into two branches. The first branch checks text is empty and prints "Error: you typed nothing. Type a whole number." The second branch checks text has a character that is not 0 to 9 and prints "Error: type a whole number with no sign." Each message then names the exact problem. A user who presses Enter by mistake sees a message about the empty line, not a message about digits that does not match what they did.

Exercise 3. Trace the function ask_yes_no from example 2 with the inputs Y , then yes, then n. For each input, write what line 4 produces, which IF matches, and what the function returns or prints.

Show the answer

For Y , line 4 removes the spaces and lowercases the letters, giving y. Line 5 matches, so the function returns TRUE. For yes, line 4 gives yes. Neither IF matches, so line 11 prints "Error: type y or n." and the loop repeats. For n, line 4 gives n. Line 8 matches, so the function returns FALSE. The second input is rejected even though a person would read it as yes, because the rule accepts only y. A stricter program would say so in the message.

Exercise 4. Write the Python for read_month() from exercise 1, then run it with the input 0, 13, abc, 7. Paste the real output.

Show the answer

def read_month():
    while True:
        text = input("Month (1 to 12): ")
        if text == "" or not text.isdecimal():
            print("Error: type a whole number.")
            continue
        month = int(text)
        if month < 1 or month > 12:
            print("Error: type a number from 1 to 12.")
            continue
        return month
 
print("Month accepted:", read_month())

Run with printf '0\n13\nabc\n7\n' | python3 month.py. The output is:

Month (1 to 12): Error: type a number from 1 to 12.
Month (1 to 12): Error: type a number from 1 to 12.
Month (1 to 12): Error: type a whole number.
Month (1 to 12): Month accepted: 7

The output shows the pattern of the lesson: each bad answer triggers one error message and one more prompt, and the good answer ends the loop.

Exercise 5. A teammate wrote this pseudocode for an age check and says it is finished. Find the mistake and fix it.

1   READ text
2   age <- text converted to a whole number
3   IF age < 0 OR age > 120 THEN
4       PRINT "Error: age must be 0 to 120."
5   END IF
6   RETURN age

Show the answer

The check is an IF, not a loop. The program asks once. If the value is out of range, line 4 prints the error and line 6 returns the bad value anyway, so the rest of the program receives 500. With the text abc, the conversion on line 2 fails before the check runs. The fix is to wrap the lines in WHILE TRUE, add a NEXT PASS OF THE LOOP after the error, move the RETURN into the loop, and check the text for non-digits before line 2. The corrected version is the pattern in example 1.

Summary

  • Input validation checks a value before the program uses it, and the three parts are the kind check, the range check and the error branch.
  • A re-prompt loop asks, checks and asks again, and it ends with a RETURN once the answer passes every check.
  • Write each check as one exact question, such as "every character is 0 to 9", so a reader and the conversion step agree on what is valid.
  • Put the check before the conversion, and keep the range check after the conversion, because a range check needs a number.
  • Reusable validators take the prompt and the limits as parameters, so one function serves many questions.
  • Test every validator with bad input as well as good input, and read the real error message when it fails.
Check yourself: Validation pseudocode (6 questions) →

Stuck? Ask the tutor

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

Sign in to ask the tutor.

← Flowcharts for loops and early exits