What you'll learn

Next, let's have a look at objects that contain a list. Examples of objects like these include objects that describe sets, for example playlists.

In the following example, we have made a class for the concept of a playlist. The playlist contains songs: songs can be added, songs can be removed, and the songs that the playlist contains can be printed.

class Playlist:

    def __init__(self):
        self.songs = []

    def add_song(self,song):
        self.songs.append(song)

    def remove_song(self,song):
        self.songs.remove(song)

    def print_songs(self):
        for song in self.songs:
            print(song)

Creating playlists is easy with the help of the class above.

from playlist import Playlist

list = Playlist()
list.add_song("22")
list.add_song("Blank Space")
list.print_songs()

Negative : 22
Blank Space

Positive : Exercise - Menu

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Positive : Exercise - Stack

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Objects in an Instance Variable List

A list that is an object's instance variable can contain objects other than strings as long as the type of objects in the list is specified when defining the list.

In the previous section, we created a class called AmusementParkRide, which was used to check whether or not a person was eligible to get on a particular ride. The Amusement park class looks like the following.

class AmusementParkRide:
     def __init__(self,name,minimum_height):
         self.name = name
         self.minimum_height = minimum_height
         self.visitors = 0

     def is_allowed_on(self,person):
         if (person.get_height() < self.minimum_height):
             return False

         self.visitors += 1
         return True

     def __str__(self):
         return self.name + ", minimum height: " + str(self.minimum_height) + ", visitors: " + str(self.visitors)

We'll extend the class so that the amusement park keeps track of the people on the ride. In this version, the ride has, as an instance variable, a list of the people who have been allowed on the ride. The list is created in the constructor.

class AmusementParkRide:
     def __init__(self,name,minimum_height):
         self.name = name
         self.minimum_height = minimum_height
         self.visitors = 0
         self.riding = []

    # ...

Let's change the method is_allowed_on. The method adds to the list all the persons who meet the height requirements.

class AmusementParkRide:
     def __init__(self,name,minimum_height):
         self.name = name
         self.minimum_height = minimum_height
         self.visitors = 0
         self.riding = []

    def is_allowed_on(self,person):
        if (person.get_height() < self.minimum_height):
            return False

        self.visitors += 1
        self.riding.append(person)
        return True

    def __str__():
        return self.name + ", minimum height requirement: " + str(self.minimum_height) +
            ", visitors: " + str(self.visitors)

Positive : Exercise - Messaging Service

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Printing an Object from a List

Let's now modify the __str__ method so that the string returned by the method contains the name of each and every person on the ride.

class AmusementParkRide:
      def __init__(self,name,minimum_height):
         self.name = name
         self.minimum_height = minimum_height
         self.visitors = 0
         self.riding = []

      def is_allowed_on(self,person):
          if (person.get_height() < self.minimum_height):
              return False

          self.visitors += 1
          self.riding.append(person)
          return True

      def __str__(self):
          # let's form a string from all the people on the list
          on_the_ride = ""
          for person in self.riding:
              on_the_ride = on_the_ride + person.get_name() + "\n"

          # we return a string describing the object
          # including the names of those on the ride
          return self.name + ", minimum height requirement: " + str(self.minimum_height) + ", visitors: " + str(self.visitors) + "\n" + "riding:\n" + on_the_ride

Let's test out the extended amusement park ride:

from amusement_park_ride import AmusementParkRide
from person import Person

matt = Person("Matt")
matt.set_weight(86)
matt.set_height(180)

ada = Person("Ada")
ada.set_weight(34)
ada.set_height(132)

megafobia = AmusementParkRide("Megafobia", 140)

print(megafobia)

print()

if (megafobia.is_allowed_on(matt)):
    print(matt.get_name() + " is allowed on the ride")
else:
    print(matt.get_name() + " is not allowed on the ride")

if (megafobia.is_allowed_on(ada)):
    print(ada.get_name() + " is allowed on the ride")
else:
    print(ada.get_name() + " is not allowed on the ride")

print(megafobia)

The program's output is:

Negative : Megafobia, minimum height requirement: 140, visitors: 0
riding:

Matt is allowed on the ride
Ada is not allowed on the ride
Megafobia, minimum height requirement: 140, visitors: 1
riding:
Matt

Even though there is no one on the ride, the string riding: is on the print output. Let's modify the __str__ method so that if there is no one on the ride, the string returned by the method informs of it.

class AmusementParkRide:
      def __init__(self,name,minimum_height):
         self.name = name
         self.minimum_height = minimum_height
         self.visitors = 0
         self.riding = []

      # ...

    def __str__(self):

        printOutput = self.name + ", minimum height requirement: " + str(self.minimum_height) + ", visitors: " + str(self.visitors) + "\n"

        if not self.riding:
            return printOutput + "no one is on the ride."

        # we form a string from the people on the list
        peopleOnRide = ""

        for person in self.riding:
            peopleOnRide = peopleOnRide + person.get_name() + "\n"

        return printOutput + "\n" + "on the ride:\n" + peopleOnRide

The print output has now been improved.

Negative : Megafobia, minimum height requirement: 140, visitors: 0
no one is on the ride.

Matt is allowed on the ride
Ada is not allowed on the ride
Megafobia, minimum height requirement: 140, visitors: 1

on the ride:
Matt

Positive : Exercise - Printing a Collection

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Clearing an Object's List

We'll next add a remove_everyone_on_ride method to the amusement park ride, which removes each and every person currently on the ride. The list method clear is very handy here.

class AmusementParkRide:
    # ...

    def remove_everyone_on_ride(self):
        self.riding.clear()

    # ...
from amusement_park_ride import AmusementParkRide
from person import Person

matt = Person("Matt")
matt.set_weight(86)
matt.set_height(180)

ada = Person("Ada")
ada.set_weight(34)
ada.set_height(132)

megafobia = AmusementParkRide("Megafobia", 140)

print(megafobia)

print()

if (megafobia.is_allowed_on(matt)):
    print(matt.get_name() + " is allowed on the ride")
else:
    print(matt.get_name() + " is not allowed on the ride")

if (megafobia.is_allowed_on(ada)):
    print(ada.get_name() + " is allowed on the ride")
else:
    print(ada.get_name() + " is not allowed on the ride")

print(megafobia)

megafobia.remove_everyone_on_ride()

print()
print(megafobia)

The program's output is:

Negative : Megafobia, minimum height requirement: 140, visitors: 0
no one is on the ride.

Matt is allowed on the ride
Ada is not allowed on the ride
Megafobia, minimum height requirement: 140, visitors: 1

on the ride:
Matt

Megafobia, minimum height requirement: 140, visitors: 1
no one is on the ride.

Calculating a Sum from Objects on a List

Let's now create a method for the amusement park ride that calculates the average height of the people currently on it. Average height can obtained by calculating the average from the persons on the ride – the average is calculated by adding up the individual values and dividing that sum by the number of values.

The implementation underneath returns -1 if not a single person is on the ride. The result of -1 is impossible in a program that calculates averages. Based on that, we can determine that the average could not have been calculated.

class AmusementParkRide:
    # ...

    def average_height_of_people_on_ride(self):
        if not self.riding:
            return -1

        sum_of_heights = 0
        for person in self.riding:
            sum_of_heights += person.get_height()

        return sum_of_heights / len(self.riding)
    # ...
from amusement_park_ride import AmusementParkRide
from person import Person

matt = Person("Matt")
matt.set_height(180)

ada = Person("Ada")
ada.set_height(132)

grace = Person("Grace")
grace.set_height(194)

megafobia = AmusementParkRide("Megafobia", 140)

megafobia.is_allowed_on(matt)
megafobia.is_allowed_on(ada)
megafobia.is_allowed_on(grace)

print(megafobia)
print(megafobia.average_height_of_people_on_ride())

The program's output is:

Negative : Megafobia, minimum height requirement: 140, visitors: 2

on the ride:
Matt
Grace

187.0

Positive : Exercise - Santa's Workshop

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Retrieving a Specific Object from a List

We'll now create a method for the amusement park ride that returns the tallest person on the ride. As such, the method should both retrieve the tallest person from the list and return it.

Methods that retrieve objects from a list should be implemented in the following way. First off, we'll check whether or not the list is empty - if it is, we return a None reference or some other value indicating that the list had no values. After that, we create an object reference variable that describes the object to be returned. We set the first object on the list as its value. We then go through the values on the list by comparing each list object with the object variable representing the object to be returned. If the comparison finds a better matching object, its assigned to the object reference variable to be returned. Finally, we return the object variable describing the object that we want to return.

def get_tallest(self):
    # return a null reference if there's no one on the ride
    if not self.riding:
        return None

    # create an object reference for the object to be returned
    # its first value is the first object on the list
    return_object = self.riding[0]

    # go through the list
    for prs in self.riding:
        # compare each object on the list
        # to the return_object -- we compare heights
        # since we're searching for the tallest,

        if (return_object.get_height() < prs.get_height()):
            # if we find a taller person in the comparison,
            # we assign it as the value of the return_object
            return_object = prs

    # finally, the object reference describing the
    # return object is returned
    return return_object

Finding the tallest person is now easy.

from amusement_park_ride import AmusementParkRide
from person import Person

matt = Person("Matt")
matt.set_height(180)

ada = Person("Ada")
ada.set_height(132)

grace = Person("Grace")
grace.set_height(194)

megafobia = AmusementParkRide("Megafobia", 140)

megafobia.is_allowed_on(matt)
megafobia.is_allowed_on(ada)
megafobia.is_allowed_on(grace)

print(megafobia)
print(megafobia.average_height_of_people_on_ride())

print()
print(megafobia.get_tallest().get_name())
tallest = megafobia.get_tallest()
print(tallest.get_name())

Negative : Megafobia, minimum height requirement: 140, visitors: 2
on the ride:
Matt
Grace

187.0

Grace
Grace

Positive : Exercise - Longest in collection

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Positive : Exercise - Height order

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Positive : Exercise - Cargo hold

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

What you'll learn

Let's examine the process of implementing a program and separating different areas of responsibility from each other. The program asks the user to write words until they write the same word twice.

Negative : Write a word: User: <carrot>
Write a word: User: <turnip> Write a word:User: <potato>
Write a word: User: <celery>
Write a word: User: <potato>
You wrote the same word twice!

Let's build this program piece by piece. One of the challenges is that it is difficult to decide how to approach the problem, or how to split the problem into smaller subproblems, and from which subproblem to start.

There is no one clear answer – sometimes it is good to start from the problem domain and its concepts and their connections, sometimes it is better to start from the user interface.

We could start implementing the user interface by creating a class UserInterface:

class UserInterface:

    def __init__(self):
        # initial variables

    def start(self):
        # do something

Creating and starting up a user interface can be done as follows.

def main():
    user_interface = UserInterface()
    user_interface.start()

Looping and quitting

This program has (at least) two "sub-problems". The first problem is continuously reading words from the user until a certain condition is reached. We can outline this as follows.

class UserInterface:

    def start(self):
        while True:
            print("Enter a word: ")
            word = input()

            if (*stop condition*):
                break

        print("You gave the same word twice!")

The program continues to ask for words until the user enters a word that has already been entered before. Let us modify the program so that it checks whether the word has been entered or not. We don't know yet how to implement this functionality, so let us first build an outline for it.

class UserInterface:

    def start(self):
        while True:
            word = input("Enter a word: ")

            if self.already_entered(word):
                break

        print("You gave the same word twice!")

    def already_entered(self,word):
        # do something here

        return False

It's a good idea to test the program continuously, so let's make a test version of the method:

def already_entered(self,word):
    if word == "end":
        return True

    return False

Now the loop continues until the input equals the word "end":

Negative : Write a word: User: <carrot>
Write a word: User: <turnip> Write a word:User: <potato>
Write a word: User: <celery>
Write a word: User: <end>
You wrote the same word twice!

The program doesn't completely work yet, but the first sub-problem - quitting the loop when a certain condition has been reached - has now been implemented.

Storing relevant information

Another sub-problem is remembering the words that have already been entered. A list is a good structure for this purpose.

class UserInterface:

    def __init__(self):
        self.words = []

    # ...

When a new word is entered, it has to be added to the list of words that have been entered before. This is done by adding a line that updates our list to the while-loop:

while True:
    word = input("Enter a word: ")

    if (self.already_entered(word)):
        break

    # adding the word to the list of previous words
    self.words.append(word)

The whole user interface looks as follows.

class UserInterface:

    def __init__(self):
        self.words = []

    def start(self):
        while True:
            word = input("Enter a word: ")

            if (self.already_entered(word)):
                break

            # adding the word to the list of previous words
            self.words.append(word)
        print("You gave the same word twice!")

    def already_entered(self,word):
        if word == "end":
            return True

        return False

Again, it is a good idea to test that the program still works. For example, it might be useful to add a test print to the end of the start-method to make sure that the entered words have really been added to the list.

# test print to check that everything still works
for word in self.words:
    print(word)

Combining the solutions to sub-problems

Let's change the method ‘already_entered' so that it checks whether the entered word is contained in our list of words that have been already entered.

def already_entered(self,word):
    return word in self.words

Now the application works as intended.

Objects as a natural part of problem solving

We just built a solution to a problem where the program reads words from a user until the user enters a word that has already been entered before. Our example input was as follows:

Negative : Write a word: User: <carrot>
Write a word: User: <turnip> Write a word:User: <potato>
Write a word: User: <celery>
Write a word: User: <potato>
You wrote the same word twice!

We came up with the following solution:

class UserInterface:

    def __init__(self):
        self.words = []

    def start(self):
        while True:
            word = input("Enter a word: ")

            if (self.already_entered(word)):
                break

            # adding the word to the list of previous words
            self.words.append(word)

        for word in self.words:
            print(word)            
        print("You gave the same word twice!")

    def already_entered(self,word):
        return word in self.words

From the point of view of the user interface, the support variable ‘words' is just a detail. The main thing is that the user interface remembers the set of words that have been entered before. The set is a clear distinct "concept" or an abstraction. Distinct concepts like this are all potential objects: when we notice that we have an abstraction like this in our code, we can think about separating the concept into a class of its own.

Word set

Let's make a class called ‘WordSet'. After implenting the class, the user interface's start method looks like this:

while True:
    word = input()

    if (words.contains(word)):
        break

    word_set.add(word)

print("You gave the same word twice!")

From the point of view of the user interface, the class WordSet should contain the method contains(self,word), that checks whether the given word is contained in our set of words, and the method add(self,word), that adds the given word into the set.

We notice that the readability of the user interface is greatly improved when it's written like this.

The outline for the class ‘WordSet' looks like this:

class WordSet:
    # object variable(s)

    def __init__(self):
        # constructor

    def contains(self,word):
        # implementation of the contains method
        return False

    def add(self,word):
        # implementation of the add method

Earlier solution as part of implementation

We can implement the set of words by making our earlier solution, the list, into an object variable:

class WordSet:
    def __init__(self):
        self.words = []

    def add(self,word):
        self.words.append(word)

    def contains(self,word):
        return word in self.words  

Now our solution is quite elegant. We have separated a distinct concept into a class of its own, and our user interface looks clean. All the "dirty details" have been encapsulated neatly inside an object.

Let's now edit the user interface so that it uses the class WordSet. The class is given to the user interface as a parameter, just like Scanner.

class UserInterface:

    def __init__(self,WordSet):
        self.word_set = WordSet

    def start(self):
        while True:
            word = input("Enter a word: ")

            if (self.word_set.contains(word)):
                break

            # adding the word to the list of previous words
            self.word_set.add(word)

        print("You gave the same word twice!")

Starting the program is now done as follows:

from user_interface import UserInterface
from word_set import WordSet

def main():
    set = WordSet()

    user_interface = UserInterface(set)
    user_interface.start()

Changing the implementation of a class

We have arrived at a situation where the class ‘WordSet' "encapsulates" a list. Is this reasonable? Perhaps. This is because we can make other changes to the class if we so desire, and before long we might arrive into a situation where the word set has to be, for example, saved into a file. If we make all these changes inside the class WordSet without changing the names of the methods that the user interface uses, we don't have to modify the actual user interface at all.

The main point here is that changes made inside the class WordSet don't affect the class UserInterface. This is because the user interface uses WordSet through the methods that it provides – these are called its deferfaces.

Implementing new functionality: palindromes

In the future, we might want to augment the program so that the class ‘WordSet' offers some new functionalities. If, for example, we wanted to know how many of the entered words were palindromes, we could add a method called ‘palindromes' into the program.

def start(self):
        while True:
            word = input("Enter a word: ")

            if (self.word_set.contains(word)):
                break

            # adding the word to the list of previous words
            self.word_set.add(word)

        print("You gave the same word twice!")
        print(str(self.word_set.palindromes()) + " of the words were palindromes.")

The user interface remains clean, because counting the palindromes is done inside the ‘WordSet' object. The following is an example implementation of the method.

class WordSet:
    def __init__(self):
        self.words = []

    def add(self,word):
        self.words.append(word)

    def contains(self,word):
        return word in self.words

    def palindromes(self):
        count = 0

        for word in self.words:
            if is_palindrome(word):
                count += 1

        return count

    def is_palindrome(self,word):
        end = len(word) - 1

        i = 0
        while (i < len(word) / 2):
            # [] returns the character at given index
            # as a simple variable
            if(word[i] != word[end-i]):
                return False

            i += 1

        return True

The method ‘palindromes' uses the helper method ‘is_palindrome' to check whether the word that's given to it as a parameter is, in fact, a palindrome.

Positive : Recycling

When concepts have been separated into different classes in the code, recycling them and reusing them in other projects becomes easy. For example, the class ‘WordSet' could be well be used in a graphical user interface, and it could also part of a mobile phone application. In addition, testing the program is much easier when it has been divided into several concepts, each of which has its own separate logic and can function alone as a unit.

Programming tips

In the larger example above, we were following the advice given here.

Programmers follow these conventions so that programming can be made easier. Following them also makes it easier to read programs, to keep them up, and to edit them in teams.

Positive : Exercise - Simple Dictionary

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Positive : Exercise - To do list

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

From one entity to many parts

Let's examine a program that asks the user to enter exam points and turns them into grades. Finally, the program prints the distribution of the grades as stars. The program stops reading inputs when the user inputs an empty string. An example program looks as follows:

Positive : Points:
User: <91>
Points:
User: <98>
Points:
User: <103>
Impossible number.
Points:
User: <90>
Points:
User: <89>
Points:
User: <89>
Points:
User: <88>
Points:
User: <72>
Points:
User: <54>
Points:
User: <55>
Points:
User: <59>
Points:
User: <41>
Points:
User: <48>
Points:

5: ***
4: ***
3: *
2:
1: ***
0: **

As with almost all programs, this program can be written into main as one entity. Here is one possibility.

def main():
    grades = []

    while True:
        textinput = input("Points: ")
        if (textinput == ""):
            break

        score = int(textinput)

        if (score < 0 or score > 100):
            print("Impossible number.")
            continue

        grade = 0
        if (score < 50):
            grade = 0
        elif (score < 60):
            grade = 1
        elif (score < 70):
            grade = 2
        elif (score < 80):
            grade = 3
        elif (score < 90):
            grade = 4
        else:
            grade = 5

        grades.append(grade)

    print("")
    grade = 5
    while (grade >= 0):
        stars = 0
        for received in grades:
            if (received == grade):
                stars += 1

        print(str(grade) + ": ")
        while (stars > 0):
            print("*")
            stars -= 1

        print("")

        grade = grade - 1

main()        

Let's separate the program into smaller chunks. This can be done by identifying several discrete areas of responsibility within the program. Keeping track of grades, including converting scores into grades, could be done inside a different class. In addition, we could create a new class for the user interface.

Program logic

Program logic includes parts that are crucial for the execution of the program, like functionalities that store information. From the previous example, we can separate the parts that store grade information. From these we can make a class called ‘GradeRegister', which is responsible for keeping track of the numbers of different grades students have received. In the register, we can add grades according to scores. In addition, we can use the register to ask how many people have received a certain grade.

An example class follows.

class GradeRegister:

    def __init__(self):
        self.grades = []

    def add_grade_based_on_points(self,points):
        self.grades.append(self.points_to_grades(points))

    def number_of_grades(self,grade):
        count = 0
        for received in self.grades:
            if (received == grade):
                count += 1

        return count

    def points_to_grades(self,points):

        grade = 0
        if (points < 50):
            grade = 0
        elif (points < 60):
            grade = 1
        elif (points < 70):
            grade = 2
        elif (points < 80):
            grade = 3
        elif (points < 90):
            grade = 4
        else:
            grade = 5

        return grade

When the grade register has been separated into a class, we can remove the functionality associated with it from our main program. The main program now looks like this.

from grade_register import GradeRegister

def main():
    register = GradeRegister()

    while True:
        textinput = input("Points: ")
        if not textinput: # same as input == ""
            break

        score = int(textinput)

        if (score < 0 or score > 100):
            print("Impossible number.")
            continue

        register.add_grade_based_on_points(score)

    print("")
    grade = 5
    while (grade >= 0):
        stars = register.number_of_grades(grade)
        print(str(grade) + ": ")
        while (stars > 0):
            print("*")
            stars-= 1

        print("")

        grade = grade - 1

Separating the program logic is a major benefit for the maintenance of the program. Since the program logic – in this case the GradeRegister – is its own class, it can also be tested separately from the other parts of the program. If you wanted to, you could copy the class GradeRegister and use it in your other programs. Below is an example of simple manual testing – this experiment only concerns itself with a small part of the register's functionality.

register = GradeRegister()
register.add_grade_based_on_points(51)
register.add_grade_based_on_points(50)
register.add_grade_based_on_points(49)

print("Number of students with grade 0 (should be 1): " + str(register.number_of_grades(0)))

User interface

Typically each program has its own user interface. We will create the class UserInterface and separate it from the main program. The user interface receives two parameters in its constructor: a grade register for storing the grades, and a Scanner object used for reading input.

When we now have a separate user interface at our disposal, the main program that initializes the whole program becomes very clear.

from grade_register import GradeRegister
from user_interface import UserInterface

def main():
    register = GradeRegister()

    user_interface = UserInterface(register)
    user_interface.start()

Let's have a look at how the user interface is implemented. There are two essential parts to the UI: reading the points, and printing the grade distribution.

class UserInterface:

    def __init__(self,register):
        self.register = register

    def start(self):
        self.read_points()
        print("")
        self.print_grade_distribution()

    def read_points(self):

    def print_grade_distribution(self):

We can copy the code for reading exam points and printing grade distribution nearly as is from the previous main program. In the program below, parts of the code have indeed been copied from the earlier main program, and new method for printing stars has also been created – this clarifies the method that is used for printing the grade distribution.

class UserInterface:

    def __init__(self,register):
        self.register = register

    def start(self):
        self.read_points()
        print("")
        self.print_grade_distribution()

    def read_points(self):
        while True:
            textinput = input("Points: ")
            if not textinput: # same as input == ""
                break

            score = int(textinput)

            if (score < 0 or score > 100):
                print("Impossible number.")
                continue

            self.register.add_grade_based_on_points(score)

    def print_grade_distribution(self):
        grade = 5
        while (grade >= 0):
            stars = self.register.number_of_grades(grade)
            print(str(grade) + ": ")
            self.print_stars(stars)
            print("")

            grade = grade - 1

    def print_stars(self,stars):
        while (stars > 0):
            print("*")
            stars-= 1

Positive : Exercise - Averages

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Positive : Exercise - Joke manager

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

What you'll learn

Let's take our first steps in the world of program testing.

Error Situations and Step-By-Step Problem Resolving

Errors end up in the programs that we write. Sometimes the errors are not serious and cause headache mostly to users of the program. Occasionally, however, mistakes can lead to very serious consequences. In any case, it's certain that a person learning to program makes many mistakes.

You should never be afraid of or avoid making mistakes since that is the best way to learn. For this reason, try to break the program that you're working on from time to time to investigate error messages, and to see if those messages tell you something about the error(s) you've made.

Positive : Software Error

The report in the address http://sunnyday.mit.edu/accidents/MCO_report.pdf describes an incident resulting from a more serious software error and also the error itself.

The bug in the software was caused by the fact that the program in question expected the programmer to use the International System of Units (meters, kilograms, ...) in the calculations. However, the programmer had used the American Measurement System for some of the system's calculations, which prevented the satellite navigation auto-correction system from working as inteded.

The satellite was destroyed, costing millions of dollars of damage.

As programs grow in their complexity, finding errors becomes even more challenging.

Stack Trace (Traceback)

When an error occurs in a program, the program typically prints something called a stack trace, i.e., the list of method calls that resulted in the error. For example, for a file that contains:

def greet(name):
    print('Hello, ' + nam)

greet('Ada')

a stack trace might look like this:

Traceback (most recent call last):
  File "/path/to/example.py", line 4, in <module>
    greet('Ada')
  File "/path/to/example.py", line 2, in greet
    print('Hello, ' + nam)
NameError: name 'nam' is not defined

The type of error is stated at the beginning of the list, and the following lines tell us where the error occurred. The type of error thrown here is a NameError, which says that Python didn't understand the variable nam (since it should have been name here).

Checklist for Troubleshooting

If your code doesn't work and you don't know where the error is, these steps will help you get started.

  1. Indent your code properly and find out if there are any missing parentheses.
  2. Verify that the variables used are correctly named.
  3. Test the program flow with different inputs and find out the sort of input that causes the program to not work as desired. If you received an error in the tests, the tests may also indicate the input used.
  4. Add print commands to the program in which you print out the values of the variables used at various stages of the program's execution.
  5. Verify that all variables you are using are initialized. If they aren't, an AttributeError error will occur.
  6. If your program causes an exception, you should definitely pay attention to the stack trace associated with the exception, which is the list of method calls that resulted in the situation that caused the exception.
  7. Learn how to use a debugger.

Automated testing

Manually testing the program is often laborious. It's possible to automate the testing by using a testing framework such as pytest. You'll find an example below of how to test a program automatically:

def ada():
    return "Ada Lovelace"
import adafile

def test_ada():
    assert adafile.ada() == "Ada Lovelace"

Each of these programs is in a different file in the same directory. The first has the filename adafile.py while the second has the filename test_adafile.py. You can run the test with the command pytest in the same directory.

Unit Testing

The automated testing method laid out above where the input to a program is modified is quite convenient, but limited nonetheless. Testing larger programs in this way is challenging. One solution to this is unit testing, where small parts of the program are tested in isolation.

Unit testing refers to the testing of individual components in the source code, such as classes and their provided methods. The writing of tests reveals whether each class and method observes or deviates from the guideline of each method and class having a single, clear responsibility. The more responsibility the method has, the more complex the test. If a large application is written in a single method, writing tests for it becomes very challenging, if not impossible. Similarly, if the application is broken into clear classes and methods, then writing tests is straightforward.

Ready-made unit test libraries are commonly used in writing tests, which provide methods and help classes for writing tests. The most common unit testing library in Python is unittest but we will advocate the usage of pytest in this course. The code you have been submitting to Github throughout this course has been unit tested along the way. Go back and look through the folders you've been submitting. You should see test files in there which are used to verify your code is correct. Explore them!

Positive : Unit Testing and the Parts of an Application

Unit testing tends to be extremely complicated if the whole application has been written in one function. To make testing easier, the app should split into small parts, each having a clear responsibility. In the previous section, we practiced this when we separated the user interface from the application logic. Writing tests for parts of an application, such as the ‘JokeManager' class from the exercise in the previous section is significantly easier than writing them for program contained in "main" in its entirety.

Test-Driven Development

Test-driven development is a software development process that's based on constructing a piece of software in small iterations. In test-driven software development, the first thing a programmer always does is write an automatically-executable test, which tests a single piece of the computer program.

The test will not pass because the functionality that satisfies the test, i.e., the part of the computer program to be examined, is missing. Once the test has been written, functionality that meets the test requirements is added to the program. The tests are run again. If all tests pass, a new test is added, or alternatively, if the tests fail, the already-written program is corrected. If necessary, the internal structure of the program will be corrected or refactored, so that the functionality of the program remains the same, but the structure becomes clearer.

Test-driven software development consists of five steps that are repeated until the functionality of the program is complete.

  1. Write a test. The programmer decides which program functionality to test and writes a test for it.
  2. Run the tests and check if the tests pass. When a new test is written, the tests are run. If the test passes, the test is most likely erroneous and should be corrected - the test should only test functionality that hasn't yet been implemented.
  3. Write the functionality that meets the test's requirements. The programmer implements functionality that only meets the test requirements. Note: this doesn't do things that the test does not require - functionality is only added in small increments.
  4. Perform the tests. If the tests fail, there is likely to be an error in the functionality written. Correct the functionality - or, if there is no error in the functionality, fix the latest test that was performed.
  5. Repair the internal structure of the program. As the size of the program increases, its internal structure is adjusted as needed. Methods that are too long are broken down into multiple parts and classes representing concepts are isolated. The tests are not modified, but are instead used to verify the correctness of the changes made to the program's internal structure - if a change in the program structure changes the functionality of the program, the tests will produce a warning and the programmer can remedy the situation.

Positive : Exercise - Testing

Read the instructions for the exercise and commit the solution via Github.

Accept exercise on Github Classroom

Positive : Unit testing

Unit testing is only a part of software testing. On top of unit testing, a developer also performs integration tests that examine the interoperability of components, such as classes, and interface tests that test the application's interface through elements provided by the interface, such as buttons.

These testing methods are covered in more detail in more advanced courses.

You can check out this excellent resource if you are interested in learning more about testing in Python.

When you learn programming, you also develop a better eye for reading code (yours and others). In this part we understood the use of lists and practiced separating the UI from the program logic.

The following is from On the role of scientific thought by Edsger W. Dijkstra .

Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one's subject matter in isolation for the sake of its own consistency, all the time knowing that one is occupying oneself only with one of the aspects. We know that a program must be correct and we can study it from that viewpoint only; we also know that it should be efficient and we can study its efficiency on another day, so to speak. In another mood we may ask ourselves whether, and if so: why, the program is desirable. But nothing is gained - on the contrary! - by tackling these various aspects simultaneously. It is what I sometimes have called "

the separation of concerns

", which, even if not perfectly possible, is yet the only available technique for effective ordering of one's thoughts, that I know of. This is what I mean by "focusing one's attention upon some aspect": it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect's point of view, the other is irrelevant. It is being one- and multiple-track minded simultaneously.

The core of Dijkstra's message is, that the problem areas of a program must be separated from each other – this is exactly what we have been doing with object-oriented programming and by separating the UI from the program logic. Each problem area has been separated into its own part. This can also be viewed through the lens of program responsibilities. In his blog Robert "Unvle Bob" C. Martin describes the term "single responsibility principle":

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function. You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.

[..in other words..] Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Proper program structure and following good naming principles leads to clean code. When you code a bit more, you'll learn that each program part can be given one clear area of responsibility.