Grade by Grade
A. Programming Concepts and Skills · Algorithm Design and Analysis

Specifying Algorithms: Preconditions, Postconditions and Loop Invariants

C2.1 A4.2About 70 minutes
Learning goals
  • I can state the precondition (starting state) and postcondition (ending state) of an algorithm.
  • I can write a precondition and postcondition in a docstring and check a precondition with assert or an exception.
  • I can find a loop invariant and use it to explain why an algorithm reaches its postcondition.
  • I can predict what goes wrong when a precondition is not met.

Words to know

precondition
What must be true before an algorithm runs (its starting state) for it to work correctly.
postcondition
What the algorithm guarantees is true when it finishes (its ending state), as long as the precondition held.
contract
The agreement between a caller and a function: the caller makes the precondition true, and the function then makes the postcondition true.
loop invariant
A statement that is true before the loop starts and stays true after every pass of the loop.
assertion
A statement in code (assert) that checks a condition and stops the program with an AssertionError if it is false.
side effect
A change a function makes outside its return value, such as changing a list it was given or printing.

A promise with fine print

A snow-clearing company promises: "Your driveway will be clear by 7 a.m." Read the fine print: if the snowfall is under 30 cm and your car is not parked in the driveway. The fine print is the precondition. The promise is the postcondition.

Algorithms work the same way. Before you can measure how fast an algorithm is (the next lessons), you need to be able to say exactly what it does and when it works. That is the job of preconditions and postconditions. This lesson is the base for the whole unit: every search, sort and recursive algorithm you write here will start with a clear statement of its starting and ending states.

Remember from Grade 11

In Algorithms to Specification: Inserting and Deleting in a Sorted Array (ICS3U) you read a specification whose precondition was "the list is sorted in ascending order" and whose postcondition was "the list contains the new value and is still sorted". Now you will analyse these conditions for any algorithm, and use them to argue that the algorithm is correct.

Starting state and ending state

The precondition describes the starting state: what must be true about the inputs (and anything else the algorithm reads) before it begins. The postcondition describes the ending state: what is true about the return value and the data when it finishes.

Together they form a contract:

  • The caller is responsible for making the precondition true.
  • The function is responsible for making the postcondition true, provided the precondition was true.
  • If the caller breaks the precondition, the function promises nothing. It might crash, loop forever, or (worst of all) return a wrong answer quietly.

A good postcondition also says what the function does not change. "The list a is unchanged" is part of the contract, because a caller may rely on it.

Precondition(starting state)a is a list ofnumbers, len(a) ≥ 1index_of_max(a)the algorithmPostcondition(ending state)returns i with a[i] ≥every element; a unchangedThe caller must make this trueThe function promises thisa contract
A contract for index_of_max. The precondition (left) is the caller's job; the postcondition (right) is the function's promise. If the left box is false, the right box is not guaranteed.

Writing the contract down

Professional programmers put the contract in the function's docstring, the documentation string right under the def line. Here is a function that finds the position of the largest element, written with its contract.

def index_of_max(a):
"""Return the index of the largest element of a.
Precondition: a is a list of numbers and len(a) >= 1.
Postcondition: returns the smallest index i such that
a[i] >= a[k] for every index k; a is unchanged.
"""
best = 0
for i in range(1, len(a)):
if a[i] > a[best]:
best = i
return best
print(index_of_max([4, 9, 2, 9, 1]))

Output: 1. There are two 9s. The postcondition says smallest index, and > (not >=) keeps the first 9, so the answer is 1.

Why "smallest index"?

A precise postcondition settles every question in advance. With two equal largest values, should the function return 1 or 3? The contract says 1. A tester can now write a test for that case, and anyone who changes > to >= (which would return 3) breaks the contract. Vague postconditions such as "returns the biggest" lead to arguments and bugs.

What if the precondition is broken?

Call index_of_max([]). The loop runs zero times and the function returns 0, but there is no element 0. The caller then does a[0] and gets an IndexError far away from the real cause. A function can check its precondition so the failure happens at the right place:

  • assert len(a) >= 1, "a must not be empty" checks a condition that should never be false if the program is correct. It is a tool for catching programmer mistakes while developing and testing.
  • raise ValueError("...") is for bad data that can really happen when the program runs, for example a user typing nothing. The caller can catch it with try/except (you met exceptions in ICS3U).
def average(values):
"""Return the arithmetic mean of values.
Precondition: values is a non-empty list of numbers.
Postcondition: returns sum(values) / len(values); values is unchanged.
"""
assert len(values) > 0, "average() needs at least one value"
return sum(values) / len(values)
print(average([12, 15, 21]))
print(average([]))

The first call meets the precondition and prints 16.0. The second call breaks it, so the program stops with AssertionError: average() needs at least one value instead of a confusing ZeroDivisionError.

assert is not input validation

Python skips every assert when it is run with the -O (optimize) option, so never use assert to check user input or file data. Use if and raise ValueError(...), or ask again. Use assert for conditions that are true in a correct program: preconditions between your own functions, loop invariants and test checks.

Loop invariants: the idea that proves a loop works

How do you know index_of_max is right for every list, not only the ones you tested? Find a loop invariant: a statement that is

  1. true before the loop starts (initialization),
  2. still true after each pass if it was true before that pass (maintenance), and
  3. useful when the loop ends: together with the reason the loop stopped, it gives the postcondition (termination).

For index_of_max the invariant is: at the start of each pass with index i, a[best] is the largest of a[0] to a[i − 1], and best is the first place it occurs.

  • Before the loop: i = 1 and best = 0. The only element checked is a[0], so it is the largest. True.
  • Each pass: if a[i] is bigger, best moves to i; otherwise best stays. Either way a[best] is the largest of a[0] to a[i]. True for the next i.
  • At the end: i has gone past the last index, so the "checked part" is the whole list. That is exactly the postcondition.
Invariant: a[best] is the largest of the shaded elements a[0] … a[i−1]i = 149291best = 0 (a[best] = 4)i = 249291best = 1 (a[best] = 9)i = 349291best = 1 (a[best] = 9)i = 449291best = 1 (a[best] = 9)end49291best = 1 (a[best] = 9)Shaded green: already checked. Yellow: the current best.
A trace of index_of_max([4, 9, 2, 9, 1]). At every row the invariant holds: the yellow cell is the largest of the shaded cells. On the last row the whole list is shaded, so the invariant becomes the postcondition.
Worked example

Analysing a contract

Here is a function that is supposed to count how many times a value appears in a sorted list by stopping early:

def count_sorted(a, x):

    count = 0

    for v in a:

        if v == x:

            count += 1

        elif v > x:

            break

    return count

State its precondition and postcondition, and show what happens if the precondition is broken with a = [5, 1, 5] and x = 5.

  1. Inputs it reads: a list a and a value x. The loop stops as soon as it sees a value bigger than x. That shortcut is only safe if every later value is also bigger, which is true when a is sorted in ascending order.
  2. Precondition: a is a list sorted in ascending order, and x can be compared with its elements.
  3. Postcondition: returns the number of indexes k with a[k] == x; a is unchanged (the function only reads it).
  4. Broken precondition: with [5, 1, 5], v = 5 matches (count = 1); v = 1 is not equal and not bigger, so the loop continues; v = 5 matches (count = 2). The answer 2 happens to be right!
  5. Try a = [5, 9, 5]: v = 5 (count = 1), then v = 9 > 5, so the loop breaks. It returns 1, but there are two 5s. The function returns a wrong answer without any error, the most dangerous kind of failure.

Precondition: a is sorted ascending. Postcondition: returns how many elements equal x, and a is unchanged. Unsorted input can give a silently wrong count (1 instead of 2 for [5, 9, 5]), which is why the precondition must be stated and, while testing, checked.

Analysing any algorithm: a checklist

When you are asked to analyse the precondition and postcondition of an algorithm:

  1. List every input it reads (parameters, global values, files).
  2. For each line, ask: what must be true for this line to work? (An index must be in range; a divisor must not be 0; a shortcut needs sorted data.) These become the precondition.
  3. Describe the return value exactly, including ties, empty cases and "not found".
  4. Say what the function changes (side effects) and what it leaves unchanged.
  5. Test the edges: the smallest legal input, and one input that breaks each part of the precondition.
Just for fun

Precondition for enjoying this joke: you have read the previous section.

Postcondition: you groan. (If you did not groan, the precondition was not met. Please re-read the section, then the joke.)

Hands-on activity

Contract cards for three functions

You need: a computer with Python, 3 index cards (or a sheet of paper folded into three), and a pen.

  1. Pick three functions you can write in a few lines: index_of_min(a), is_sorted(a) (returns True if every element is ≤ the next one), and remove_all(a, x) (removes every copy of x from the list a, changing a).
  2. On each card, write the precondition on the front and the postcondition on the back before you write any code. Be exact: ties? empty list? what changes?
  3. Code each function with its contract in the docstring, and an assert that checks the precondition where it makes sense.
  4. Swap cards with a family member. Ask them to suggest an input that might break the function ("What about an empty list?"). Run it. Did the contract cover it?
  5. For index_of_min, write the loop invariant as a comment above the loop, and add assert lines that check it on each pass, for example assert a[best] == min(a[:i]) at the top of the loop body. Run it on 5 different lists. Then delete the invariant checks (they make the loop slow).

What to look for: is_sorted([]) and is_sorted([7]) should both return True (there is no pair out of order), and remove_all should say in its postcondition that the order of the remaining elements is unchanged.

Your turn

1

What is the difference between a precondition and a postcondition?

2

Write a precondition for def divide_evenly(total, people): return total // people.

3

Write a postcondition for a function is_sorted(a) that returns True or False.

4

Who is responsible for making the precondition true: the function or the caller?

5

A function sorts the list it is given in place and returns None. Write its postcondition.

6

Why is assert a bad way to check that a user typed a positive number?

7

Give the three properties a loop invariant must have to prove a loop correct.

More practice: warm-up

1

True or false: if the precondition is broken, the function must raise an error.

2

Name the starting state and the ending state using the course vocabulary.

3

What is a side effect? Give one example.

4

Write an assert that checks that a list scores is not empty, with a message.

5

What does index_of_max([3, 3, 3]) return, using the code in the lesson?

More practice: on your own

1

State a precondition and postcondition for def last_char(s): return s[-1].

2

This function is meant to return the index of x in a, or −1:

def find(a, x):

    for i in range(len(a)):

        if a[i] == x:

            return i

Which part of a sensible postcondition does it fail, and how do you fix it?

3

Give a loop invariant for this loop, which adds up a list:

total = 0

for i in range(len(a)):

    total += a[i]

4

def middle(a): return a[len(a) // 2]. What is the precondition? Which element does it return for a list of length 4?

5

A function remove_first(a, x) removes the first copy of x from a. Write a complete postcondition, including the case where x is missing.

Challenge

1

Selection sort's outer loop (from ICS3U) has index p. State a loop invariant that, at the end, gives "the list is sorted".

2

A program calls count_sorted(scores, 70) from the worked example right after scores.append(new_score). Explain the bug and two ways to fix it.

3

Prove with a loop invariant that this function returns True exactly when a contains no negative number:

def all_non_negative(a):

    i = 0

    while i < len(a) and a[i] >= 0:

        i += 1

    return i == len(a)

Check your understanding

Quiz

1.

Which statement describes a postcondition?

2.

def ratio(a, b): return a / b. Which is the best precondition?

3.

If a caller breaks a function's precondition, what does the contract guarantee?

4.

When should a program use raise ValueError rather than assert?

5.

For the summing loop total += a[i] over i = 0 to n − 1, which is a correct loop invariant at the start of each pass?

6.

count_sorted([5, 9, 5], 5) from the lesson returns 1. Why?

For parents and teachers

Big idea: This lesson covers C2.1 (analysing the precondition, or starting state, and postcondition, or ending state, of an algorithm). It also starts A4.2, because a precise contract tells you exactly which test cases to write.

How to help: Ask your teen to explain one of their functions to you as a "deal": "If you give me ___, I promise to give you ___." Then ask, "What if I give you an empty list? Two equal values? Nothing at all?" Each question they cannot answer is a gap in the contract.

Common difficulties:

  • Writing postconditions that describe how the code works ("loops through the list") instead of what is true at the end.
  • Forgetting ties, empty inputs and the "not found" case.
  • Using assert to validate user input.
  • Choosing an invariant that is true only at the end (such as "total is the sum of the whole list"), instead of one that is true on every pass.