Menu

Producing Robust Programs

Defensive Design

Contents

  1. Defensive design
  2. Input sanitisation
  3. Input validation
  4. Validation checks
  5. Validation loops
  6. Authentication
  7. Maintainability
  8. Comments

Defensive design is planning and writing a program so it keeps behaving correctly when users do unexpected things, is difficult to misuse and stays easy to maintain. A program built this way is robust. Robustness comes from expecting that some users will act unexpectedly, by accident or on purpose.

Defensive design considerations

  • Anticipating misuse – planning safeguards for unintended uses.
  • Authentication – confirming who a user is before they reach the program or its data.
  • Input validation – checking that input is sensible before it is used.
  • Maintainability – keeping code easy to read, fix and update.

Anticipating misuse

Anticipating misuse means thinking ahead about accidental and deliberate uses you did not intend, and building safeguards before anyone runs the program. A programmer must cater for all likely input values, not only the expected ones.

For example, a school library program asks for a book number from a printed list. The expected values are 100 to 199. Likely values also include 99 and 200 from a misread list, 1O5 with a letter O instead of zero, and a blank if Enter is pressed by mistake. Each is a case to plan for.

Without defensive design

Unexpected useUnhandledHandled defensively
Typing a word where a number is neededThe program may crash with a run-time errorA clear message explains what to type and asks again
Pressing Enter with no inputA blank value is storedThe user is told a value is needed
Typing 9000 for a quantityNonsense totals come from invalid dataThe value is checked against allowed limits before use

Unhandled cases can make a program crash, behave incorrectly, store invalid data or reveal data it should keep back.

Defensive design reduces misuse by checking input, limiting login attempts, giving clear error messages and not trusting data just because a user entered it. A helpful message says exactly what went wrong and what to do next, such as “Please type a book number from 100 to 199.” A good program should explain the problem and carry on rather than simply stop with an error.

Input sanitisation means cleaning data before the program uses it. It removes or replaces characters that are not expected or allowed, so only acceptable characters reach the part of the program that stores, displays or processes the value.

You sanitise input to anticipate misuse. Someone might type a symbol by accident, or send unusual characters deliberately to see whether the program behaves differently. Filtering those characters out first reduces the chance of a crash, messy data or unexpected behaviour.

Sanitisation changes or filters the data; it does not simply check it and reject it. For example, a game profile name might allow letters and digits only. If you type Alex-R9!, sanitisation could keep AlexR9 and drop the hyphen and exclamation mark. The cleaned name is then used. This is different from validation, which tests whether data is sensible and rejects it if it is not. A program can sanitise input and validate it.

profile = input("Enter a profile name: ")
allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
clean = ""
for character in profile:
    if character in allowed:
        clean = clean + character
print(clean)

Output when the user types Alex-R9!:

Enter a profile name: Alex-R9!
AlexR9

The program builds a new string containing only letters and digits. Any other character is skipped, so the cleaned value is what gets printed and used later.

Input validation is a defensive design technique: it checks what a user has typed before the program uses it. The check compares the input with rules you set, such as a number falling inside an allowed range, or an answer matching one of the options offered. If the input breaks a rule, the program refuses it and asks again.

Validation is the usual way to deal with invalid input, because users mistype, leave boxes empty or try to misuse a program. A value of the wrong type or wildly out of range can give a wrong result or stop the program with a run-time error, so checking first keeps the program running with sensible data.

Checking and re-asking

A common pattern is a loop that reads the input, tests it, and only lets the program move on once the value passes. Each time the rule is broken, a short message explains it and the question is asked again.

tickets = int(input("How many tickets? "))
while tickets < 1 or tickets > 6:
    print("Enter a whole number from 1 to 6.")
    tickets = int(input("How many tickets? "))
print("Booking accepted for", tickets, "tickets")

Output when the user types 0, 4:

How many tickets? 0
Enter a whole number from 1 to 6.
How many tickets? 4
Booking accepted for 4 tickets

The rule here is a whole number of tickets from 1 to 6. Typing 0 makes the condition true, so the loop repeats and the question is asked again; typing 4 makes it false, so the loop ends and the booking goes through. The loop only compares numbers, though: if the user types text, int() cannot convert it, so the program still stops with an error.

Reasonable, not correct

Validation can show that data is reasonable: it follows the rules you wrote. It cannot show that the data is correct. A range check accepts 5 tickets because 5 is between 1 and 6, but it cannot know whether the user really meant 5 rather than 4. The program has checked the format and range, not the user's intention.

A validation check tests input against a rule before the program uses it. If it passes, the input is accepted; if it fails, the program can reject it and say what was expected. Choose a check that matches the data.

CheckWhat it testsExample rule
Rangethe value is between lower and upper limitsclub trip places must be 1 to 6
Typethe data is the expected type before use or conversionplaces must be a whole number, so 2.5 fails
Lengththe number of characters is within limitsa locker code must be 3 to 5 characters
Presencesomething has been entereda saved game title cannot be empty
Formatthe characters follow a required patterna gym membership code is three letters then two digits, such as ABC12
Look-up (table)the value is one of a set of allowed valuesa canteen meal must be hot, cold or salad

Check both ends of a range

A range check must test both limits. Testing only the bottom limit lets 500 places through, and testing only the top limit lets -4 through. Join the two tests with or so anything outside either limit is rejected: if count < 1 or count > 6. Use the comparison that matches each boundary so an allowed limit is accepted.

Type checks come before conversion

input() always gives text, so a number of places of 4 arrives as "4". int() converts digits but stops with an error on other text. Check the expected type first, then convert only once it passes.

Combining checks

One rule is often not enough: a number of places may need presence, type and range checks; a membership code may need length plus format. Empty input also fails a length check because its length is 0. Rules must fit the data and purpose: a range check suits a number of places, not a name, and limits should reflect what the program allows.

places = input("How many places? ")
valid_number = len(places) > 0        # presence check using length
for ch in places:
    if ch < "0" or ch > "9":
        valid_number = False           # type check: every character a digit
if not valid_number:
    print("Enter a whole number of places")
else:
    count = int(places)                # safe to convert now
    if count < 1 or count > 6:
        print("Choose between 1 and 6 places")
    else:
        print(count, "places booked")

Output when the user types 4.5:

How many places? 4.5
Enter a whole number of places

A validation loop checks input against a rule and asks again when the rule is broken. The asking happens inside a condition-controlled loop, so the same question repeats until an acceptable value is entered. A range check can be written this way: the loop condition tests whether the value is below the minimum or above the maximum.

seat_count = int(input("How many seats? "))
while seat_count < 1 or seat_count > 50:
    print("Enter a whole number from 1 to 50.")
    seat_count = int(input("How many seats? "))
print("Booking", seat_count, "seats.")

Output when the user types 0, 4:

How many seats? 0
Enter a whole number from 1 to 50.
How many seats? 4
Booking 4 seats.

The loop above keeps asking while the number of seats is outside 1 to 50. Enter 0 and the message appears before the question is asked again; the loop only ends when an allowed value is typed. This stops the program using a value that would give a wrong result.

The range check cannot run if the text cannot be converted, though. If you type many where a whole number is expected, int() cannot convert it, so Python raises a ValueError and the program stops with a run-time error before the comparison is made. The loop never gets the chance to ask again, so the program crashes instead of coping with the bad input.

Catching a conversion error

Python can also catch that error with try and except, although this is beyond what you need to write. The risky conversion goes in the try block; if it fails, control jumps to the except block and the remaining lines of the try block are skipped. Putting this inside a loop that repeats until an acceptable value has been stored means the question is asked again after an error.

seat_count = 0
while seat_count < 1 or seat_count > 50:
    try:
        seat_count = int(input("How many seats? "))
        if seat_count < 1 or seat_count > 50:
            print("Enter a whole number from 1 to 50.")
    except ValueError:
        print("Please type the seat count as digits.")
print("Booking", seat_count, "seats.")

Output when the user types many, 6:

How many seats? many
Please type the seat count as digits.
How many seats? 6
Booking 6 seats.

This catches text that is not a number, but it does not make a range check unnecessary: a whole number that is too small or too large is still the right data type, so it must be tested separately. Python says an error is raised; other languages say an exception is thrown. Invalid input is handled with validation: try and except does not replace it.

Authentication means confirming the identity of a user. It must happen before the user is allowed to use the program or reach the data it holds, so that a stranger cannot open someone else's saved work or change their scores. Without it, everything in the program is open to anyone who runs it.

Simple authentication

Simple authentication asks for a username and a password and compares each one with the details the program has stored. Both must match: a correct username with the wrong password must still be refused.

When a login fails, the message should not reveal which part was wrong. Telling someone 'that password is incorrect' confirms the username is real, so they only have to keep guessing passwords. One plain message such as 'login unsuccessful' gives nothing away.

Details enteredMessage shown
Correct username and correct passwordAccess granted
Correct username, wrong passwordLogin unsuccessful
Unknown username, any passwordLogin unsuccessful

Limiting failed attempts

A login that can be tried for ever can be beaten by guesswork, so the program should count failed attempts and stop after a set number. Access is then refused, or the account is locked until someone unlocks it. The program keeps a counter, and a condition-controlled loop stops as soon as the count runs out.

# stored details would come from a file in a real system
correct_user = "artclub7"
correct_pass = "paint42"
tries_left = 4
signed_in = False

while tries_left > 0 and signed_in == False:
    user = input("Enter username: ")
    password = input("Enter password: ")
    if user == correct_user and password == correct_pass:
        signed_in = True
    else:
        tries_left = tries_left - 1
        print("Login unsuccessful")

if signed_in == True:
    print("Gallery open")
else:
    print("Account locked - ask the club leader")

Output when the user types artclub7, blue11, artfan, paint42, artclub7, paint42:

Enter username: artclub7
Enter password: blue11
Login unsuccessful
Enter username: artfan
Enter password: paint42
Login unsuccessful
Enter username: artclub7
Enter password: paint42
Gallery open

Four tries are allowed here. Each failed pair of entries takes one off the counter, the loop ends when no tries are left, and every failure gives the same message, which tells an attacker nothing.

Strong passwords and stored details

Authentication is only as good as the passwords it accepts, so the program should require strong passwords, reasonably long and mixing letters, digits and symbols. A rule in the program can reject anything shorter than a set length, and a sensible program refuses to create an account without one.

In a real system, the stored usernames and passwords live in a file or database rather than being written into the program code. Anyone who reads the code would then know every password, and changing one would mean editing and redistributing the program itself.

Maintainability is a measure of how straightforward a program is to follow, put right and extend later on. A maintainable program can be picked up months later by the person who wrote it, or by a completely different programmer, and changed without a struggle.

Programs are rarely finished. Errors are found after release, users ask for new features and rules change, so the code you write today will probably be edited many times. If only the original author can follow the code, every change is risky: a small fix can break something else because nobody can see how the parts fit together. Maintainability is about the shape of the code, not about testing it.

Techniques that improve maintainability

  • Sub programs split a long program into named tasks. Each task is written once and called by name, so the main program reads like a short list of steps, and a change to that task only has to be made in one place.
  • Naming conventions mean choosing names that describe the value stored, such as price_per_student rather than p. A meaningful name saves the reader from tracing back through the code to work out what a variable holds, and one consistent style makes the whole program look familiar.
  • Indentation shows which lines belong inside an if, elif, else, for or while, so the structure of the program can be seen at a glance. It matters most where blocks are nested and the reader has to work out which lines belong to which construct.
  • Comments are notes beginning with # that explain what a section does, or the reasoning behind a choice. The translator ignores them, so they are written for people. A good comment explains the purpose of the code; a comment that simply repeats the line beneath it adds nothing.
# works out the total price of a trip
def trip_total(students, price_each):
    return students * price_each

student_count = int(input("How many students? "))
price_per_student = float(input("Price per student in pounds? "))
total = trip_total(student_count, price_per_student)
print("Trip total: " + str(total))

Output when the user types 24, 12.5:

How many students? 24
Price per student in pounds? 12.5
Trip total: 300.0

This program is easy to maintain because the calculation sits in a sub program named trip_total, the variables say what they store, the body is indented and a short comment explains what the sub program is for. Another programmer could change the pricing rule by editing one line.

A comment is a note written inside a program for the people who read the code. It might explain what a section does, why an approach was chosen, or what an odd-looking value means. Comments are not instructions to the user, and they are not part of the program's logic.

The translator ignores every comment: comments are stripped out before the code runs, so adding, changing or deleting one never alters the program's output or behaviour. A comment exists purely to help a human being understand what is going on.

How to write a comment in Python

In Python a comment starts with a hash symbol #. Everything from the # to the end of that line is treated as a comment, so a comment can sit on its own line above the code it describes or at the end of a line of code.

# Work out the price of a cinema trip for a group
number_of_tickets = int(input("How many tickets? "))
ticket_price = 6.50

# Groups of four or more get a 10% discount
if number_of_tickets >= 4:
    total = number_of_tickets * ticket_price * 0.9
else:
    total = number_of_tickets * ticket_price

print(total)  # price after any discount

Output when the user types 3:

How many tickets? 3
19.5

Why commenting helps

Programs are changed far more often than they are first written. A maintainer — the original programmer months later, or somebody else taking over — must work out what the code was meant to do before safely fixing a bug or adding a feature. Comments that explain a section's purpose make that quicker and reduce the chance of a change breaking something. They help with debugging too, because a note saying what a block should produce makes it easier to spot where it goes wrong.

Good comments and weak comments

The best comments explain purpose or logic — the reason behind the code. Weak comments simply repeat what the line already says, or give instructions to a user instead of explaining the code.

  • Useful: noting that a total is multiplied by 1.2 to add delivery and packing charges.
  • Useful: noting that a loop keeps asking until the value is within the permitted limits.
  • Weak: writing # add one to counter above counter = counter + 1, which is already obvious.
  • Weak: writing # type your name below, which instructs the user rather than describing the code.

Commenting sits alongside sub programs, meaningful naming conventions and indentation as a way of making a program easier to maintain, but it does not change what the program does.

Also useful