Overview

Teaching: 5 min
Exercises: 10 min
Questions
  • What sorts of things frequently go wrong in programs?

  • How can I make my programs more robust?

Objectives
  • Diagnose corner cases in simple programs.

  • Write assertions with informative messages that test for corner cases in simple programs.

Fail early, fail loudly.

Can use if, print, and sys.exit to detect and report errors.

import sys

def average(values):
    if len(values) == 0:
        print('Error in average: no values supplied')
        sys.exit(1)

    return sum(values) / len(values)

Use assert to check internal correctness.

import sys

def average(values):
    assert len(values) > 0, 'No values supplied'
    return sum(values) / len(values)

Practice defensive programming.

def kelvin_to_celsius(k):
    assert k >= 0.0, 'Temperature in Kelvin cannot be negative'
    return k - 273.15

Practice test-driven development.

assert count_leading_zeros([]) == 0
assert count_leading_zeros([0]) == 1
assert count_leading_zeros([0, 1]) == 1
assert count_leading_zeros([0, 1, 0]) == 1
assert count_leading_zeros([0, 0, 1]) == 2
assert count_leading_zeros([1, 0]) == 0

Is This Needed?

The os library contains a function called os.path.exists(path) that returns True if the file or directory identified by path exists, and False if it does not. A colleague of yours routinely uses it to check whether a file exists before trying to open it:

...
assert os.path.exists(filename), 'File {0} not found'.format(filename)
reader = open(filename, 'r')
...

Should you adopt this practice? Why or why not?

Finding Corner Cases

  1. Under what circumstances will the following function fail with an error?
  2. Under what circumstances will it return something other than what the user would expect?
def find_two_smallest(values):
    "Find the two smallest values in a list."

    copy = values[:]
    copy.sort()
    return copy[:2]

Test-Driven Development.

  1. Explain in simple language what run_starts function is supposed to do.
  2. Fill in the blanks in the function definition so that all of the assertions succeed.
  3. For what input(s) could different users reasonably expect this function to return different values? I.e., where could reasonable people still disagree about the function’s behavior?
  4. Add one or more assertions to test for each situation identified above.
def run_starts(values):

    if values == []:
        ____

    result = []
    previous = values[0] + 1
    for v in values:
        if v < previous:
            ____
            previous = ____

    return result

assert run_starts([]) == []
assert run_starts([1]) == [1]
assert run_starts([1, 2]) == [1]
assert run_starts([1, 2, 1]) == [1, 1]
assert run_starts([1, 2, 3, 2, 3]) == [1, 2]
assert run_starts([2, 3, 4, 0, 5, 7, 4, 6]) == [2, 0, 4]

Key Points