Python: testing and loops

Python code

We’ll start with the Python code we ended with last time, in a file called registration.py:

def add_shipping(subtotal: int) -> int:
    """add shipping amount to subtotal"""
    if subtotal <= 50:
        return subtotal + 10
    elif subtotal > 50 and subtotal <= 100:
        return subtotal + 20
    else:
        return subtotal + 30

def register(reg_list: list, course: str):
    """add the course to the reg_list"""
    reg_list.append(course)

Testing with pytest

Unlike Pyret (with where-blocks), Python does not have a built-in way to write tests. For this class, we’re using a Python library called pytest for testing. You’ll write your tests in a separate file. Since our file is called registration.py. we’ll call our test file test_registration.py. We can write a test for our add_shipping function:

from registration import *

def test_add_shipping():
    assert add_shipping(40) == 50
    assert add_shipping(50) == 60
    assert add_shipping(51) == 71
    assert add_shipping(100) == 120
    assert add_shipping(101) == 131

The first line imports our definitions from the registration.py file. Our test is called test_add_shipping; test names have to start with test_. The test is a function with a number of assert statements; think of assert f(x) == y in a Python test as being just like f(x) is y in a Pyret where-block.

A better registration function

What happens if we do this in the Python console?

> taking = ["CSCI0111"]
> register(taking, "CSCI0111")

It seems like we shouldn’t be able to register for the same course twice! So, let’s write a new register function:

def register(reg_list: list, course: str):
    if course not in reg_list:
        reg_list.append(course)

Notice: this if doesn’t have an else! Why is this allowed in Python but not Pyret?

Right now, register just fails silently if we try to re-register for a course. We should probably throw an error when this happens:

def register(reg_list: list, course: str):
    if course in reg_list:
        raise Exception("Already registered")
    else:
        reg_list.append(course)

Let’s test this function. Over in test_registration, we can do:

# at top
import pytest

def test_register():
    courses = []
    register(courses, "CSCI 0111")
    assert courses == ["CSCI 0111"]
    pytest.raises(Exception, register, courses, "CSCI 0111")

Let’s get loopy

Let’s say we want to sum the numbers in a list. We know how to do this in Pyret:

fun sum(lst :: List<Number>) -> Number:
  cases (List) lst:
    | empty => 0
    | link(fst, rst) => fst + sum(rst)
  end
end

In Python, we’ll do things a little differently. We’ll use the fact that we can change the value of a variable and write a loop over every element of the list. It looks like this:

def sum(lst: list) -> int:
    running_sum = 0
    for n in lst:
        running_sum = running_sum + n
    return running_sum

We can find the components of the Pyret cases version here. At the beginning, running_sum is zero; that’s just like the empty case. Inside the loop we’re adding n to the running_sum, just like we’re adding fst to sum(rst). So: what’s this for-loop doing? It’s taking each element of the list, naming it n, and letting us do something with it–in this case, adding it to running_sum.

So, how would we write a function that takes a list of classes and returns a list of all the classes in a particular department?

def courses_in_dept(courses: list, dept: str) -> list:
    found_courses = []
    for course in courses:
        if course.startswith(dept):
            found_courses.append(course)
    return found_courses