Defensive design and validation
Anticipating misuse; presence, type, range, length, format and look-up checks.
Do this lesson in the simulatorEvery program so far has trusted its user. Type twenty where a number is wanted and it crashes; type 500 and the robot drives off the table. Real users mistype, misunderstand, and sometimes try to break things on purpose. Defensive design means writing programs that expect this and cope with it. The first line of defence is validation: checking input before using it.
What goes wrong without it
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
far = int(input("How far shall I drive, 1 to 50 cm? "))
forward(60, distance=far)
print("drove", far, "cm")
Run it four times, typing 20, then nothing (just press Enter), then lots, then 500. Two of those crash with a ValueError, and one drives far off the mat. None of them is the user's fault in a way the robot can blame them for: the program should have checked.
The validation checks
| Check | Question it asks | Example |
|---|---|---|
| Presence | Was anything entered at all? | the answer is not empty |
| Type | Is it the right kind of data? | it is a whole number, not lots |
| Range | Is it between sensible limits? | it is from 1 to 50 |
| Length | Does it have the right number of characters? | a robot name of 3 to 12 letters |
| Format | Does it match a pattern? | a class code of six capital letters and digits |
| Look-up | Is it one of the allowed values? | the colour is red, green or blue |
Validation checks that data is sensible, not that it is correct. A range check accepts 30 when the user meant 20; it only rejects answers that could never be right.
Checking in order
Some checks only make sense after others: you cannot check the range of something that is not a number. So check presence, then type, then range:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
answer = input("How far, 1 to 50 cm? ")
if answer == "":
print("you must type something")
elif not answer.isdigit():
print("that is not a whole number")
elif not 1 <= int(answer) <= 50:
print("it must be between 1 and 50")
else:
forward(60, distance=int(answer))
print("drove", answer, "cm")
isdigit() is True only when every character is a digit, so it is False for "", "lots", "-5" and "2.5". Only after it passes is int() safe to call.
Asking until it is valid
Rejecting bad input is only half the job. A friendly program asks again, which is a while loop around the checks:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def ask_distance():
"""Keep asking until the answer is a whole number from 1 to 50, then return it."""
while True:
answer = input("How far, 1 to 50 cm? ")
if answer == "":
print("you must type something")
elif not answer.isdigit():
print("that is not a whole number")
elif not 1 <= int(answer) <= 50:
print("it must be between 1 and 50")
else:
return int(answer)
tone(220, 0.2)
far = ask_distance()
forward(60, distance=far)
print("drove", far, "cm")
Putting the validation in a function means the rest of the program can rely on getting a good value, and the same checks can be used every time the program asks.
Length, format and look-up
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
name = input("Robot name, 3 to 12 letters? ")
if not 3 <= len(name) <= 12:
print("length check failed")
elif not name.isalpha():
print("format check failed: letters only")
else:
print("hello,", name)
colour = input("Light colour? ").lower()
if colour in ["red", "green", "blue"]:
led(colour)
else:
print("look-up check failed: choose red, green or blue")
.lower() before a look-up check means Red and RED are accepted too: being defensive also means not rejecting answers for no good reason.
Anticipating misuse
Validation deals with the input you expect. Defensive design also asks: what could a user do that you did not expect? Type a thousand characters? Give a negative speed? Keep the robot driving towards a wall? For a robot, some checks belong in the program even when the input is valid: never drive when distance() says there is a wall 5 cm ahead, whatever the user asked for.
Task: a validated drive
Write a program that asks How far, 1 to 50 cm? until the answer is valid, then drives that far forward. For an empty answer print you must type something; for one that is not a whole number print that is not a whole number; for a number out of range print it must be between 1 and 50. The task types an empty answer, then fifty, then 500, then 30.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
far = int(input("How far, 1 to 50 cm? "))
forward(60, distance=far)
Challenges
- Accept distances with a decimal point, such as
12.5, while still rejectingtwelve. - Add a check that refuses to drive if
distance()shows a wall closer than the distance asked for, plus 5 cm. - Validate a class code: exactly six characters, only capital letters and digits.