More Programming in Python
Mutation testing: how we grade your tests
A lot of work is autograded in this course, including the tests.
We grade student tests by checking that they:
- Pass on correct code
- Fail on incorrect code
In our autograder, the first item is required before it moves on to the second item. I.e., tests must first pass on correct code before we check whether they fail on incorrect code.
Checking whether tests fail on incorrect code is a standard software engineering technique called "Mutation testing": programmers insert small, common bugs into their code and check whether the tests catch them.
You are not required to perform your own mutation testing in this course, but your assigmnment submissions must pass our mutation tests.
Disclaimer: All bugs that we inserted into the incorrect code are intended to be simple and common. If the autograder says there is a bug that remains undetected by your tests, look for large missing test cases, rather than digging into obscure ways code can run incorrectly.
Poll: Why is this assignment submission not receiving full points?
def add(a: int, b: int) -> int:
"""Returns the sum of two integers."""
return a + b
class TestAddFunction(unittest.TestCase):
"""Unit tests for the add function."""
def test_add_positive_numbers(self) -> None:
"""Test adding two positive numbers."""
self.fail()
def test_add_negative_numbers(self) -> None:
"""Test adding two negative numbers."""
self.assertEqual(add(-1, -1), -2)
def test_add_mixed_numbers(self) -> None:
"""Test adding a positive and a negative number."""
self.assertEqual(add(-1, 1), 0)
def test_add_zero(self) -> None:
"""Test adding zero to a number."""
self.assertEqual(add(0, 5), 5)
self.assertEqual(add(5, 0), 5)
- The student implemented
add()incorrectly. - Mutation testing: the student's tests don't cover enough cases.
- The student's test fails on correct code, so mutation tests are not run.
- It's something else -- pylint warnings, infinite loop, etc.
None
None works like a value that represents the absence of a value.
bodyguard_name: str = None # doesn't have a value -- I don't have a bodyguard
It's different from "" or 0 (see Null Island)
Can store None in a list
grades: list[int] = [5, None, 0]
Cannot add None to a number or string
None + "hi"does not worklen(None)does not work
Optional
To specify that a type might be None, we use Optional. For example:
from typing import Optional
def get_number_or_None(hopefully_a_number: str) -> Optional[int]:
try:
return int(hopefully_a_number)
except ValueError:
return None
Data structures (list, set, and dict)
We will have lectures dedicated to lists and sets later on, but here is the basic syntax to create them:
nums: list[int] = [1, 2, 3]
words: set[str] = {'hi', 'hello', 'howdy'}
We're assuming you have used lists before. Sets may be new to some.
A set is very similar to a list: it is a collection of items.
Differences between a set and a list:
- A set is unordered
- A set can only hold each item (at most) once -- no duplicates
Reading and writing text files
Just as we can read user input using input(prompt), we can also read input from a file:
with open('story.txt', 'r', encoding="utf-8") as file:
for line in file.readlines():
print(line)
The readlines() function returns a list of strings: each line in the file is a string in the list.
If, instead of reading from the file, we want to write to the file, then we must use a different option than 'r'.
open('story.txt', 'r'): read the fileopen('story.txt', 'w'): write the file (overwrite it if it already exists)open('story.txt', 'a'): append to the end of the file (and create the file if it doesn't exist)
We can then write to the file using file.write("Line to write to file").
Error handing
There is a control structure that we have not introduced until now: try / except
a: int = 4
b: int = 0
try:
result = a / b
print(result)
except ZeroDivisionError:
print("Cannot divide by zero")
It allows us to try to run risky code, and if an error is raised during that risky code, then it jumps immediately to the corresponding except block.
It is acceptable to use try / except blocks while testing whether a function raises an error: it is an alternative to using the built-in self.assertRaises().
Otherwise, we try to minimize the use of try / except, and only use it when absolutely necessary. We don't want to simply avoid fixing legitimate bugs by wrapping our code in a try / except.
Places where try / except is commonly used:
- Converting values
def get_user_age() -> int:
"""Get a numerical age from the user"""
user_input: str = input("Enter your age: ")
try:
age: int = int(user_input)
return age
except ValueError:
print("Please enter a valid number")
return -1
- Operations that rely on external things like network requests or database operations
- Reading from files (though using a
withblock, as we have been doing, is recommended instead)
Keywords in a try / except block:
- Each error that can be raised should get its own
exceptblock. It is okay to have multipleexceptblocks for the sametryblock. - One
exceptblock can handle multiple errors, if they require the same process:except (ValueError, TypeError) as e: - Inside an
exceptblock, we may choose toraisea different error. - If there is a
finallyblock at the end of a try / except block, then it is run in all cases (whether thetrywas fully executed, or it jumped to theexcept. - If there is an
elseblock at the end of a try / except block, then it is run only if thetrywas fully executed (and it never jumped to anexceptblock
Best practices:
- Only use try / except for the few legitimate reasons, not for control flow of the program
- Make the errors handled in
exceptblocks as specific as possible. It is okay to list multiple specific errors in the sameexceptblock.
Poll: What is output?
def noodle(hopefully_a_number: str) -> None:
try:
num: int = int(hopefully_a_number)
print('Cats rule')
except AssertionError as e:
print(f'{hopefully_a_number} is not a number')
noodle('hello')
- Cats rule
- hello is not a number
- Cats rule hello is not a number
- No output - it raises the error
Poll: What is output?
def noodle(hopefully_a_number: str) -> None:
try:
num: int = int(hopefully_a_number)
print('Cats rule')
except ValueError as e:
print(f'{hopefully_a_number} is not a number')
noodle('hello')
- Cats rule
- hello is not a number
- Cats rule hello is not a number
- No output - it raises the error