Practical session

Duration3h45

Presentation & objectives

In this session, you will solve some exercises to familiarize with defensive exceptions and assertions, and their use for defensive programming. You will also write your first unit tests, and focus on documentation.

Important

The aim of this session is to help you master important notions in computer science. An intelligent programming assistant such as GitHub Copilot, that you may have installed already, will be able to provide you with a solution to these exercises based only on a wisely chosen file name.

For the sake of training, we advise you to disable such tools first.

At the end of the practical activity, we suggest you to work on the exercise again with these tools activated. Following these two steps will improve your skills both fundamentally and practically.

Also, we provide you the solutions to the exercises. Make sure to check them only after you have a solution to the exercises, for comparison purpose! Even if you are sure your solution is correct, please have a look at them, as they sometimes provide additional elements you may have missed.

Activity contents

1 — Extract a list of floats

We have a list l containing elements of different types. for example: l = [1, '3.14', 2.8, [], (2, 5), 'hello', {'d': 3, 'e': 2.145}].

We also have the following function which extracts all the floats from a given list to feed and returns a new list floats_in_list:

# Needed imports
from typing import List, Any



def list_of_floats (composite_list: List[Any]) -> List[float]:

    """
        This function checks the contents of the list to extract the floats inside.
        In:
            * composite_list: A list with various types.
        Out:
            * A list with the floats found in composite_list.
    """

    # Extract floats from the list
    floats_in_list = []
    for value in composite_list:
        if type(value) in (int, float):
            floats_in_list.append(float(value))
    return floats_in_list

Test this function on the example list l above. What do you notice?

Rewrite the function to take account numbers in the form of strings. To do this, you can use the string.digits property.

Solution to the exercise
# Needed imports
from typing import List, Any
import string



def is_integer (s: str) -> bool:

    """
        This function verifies if a given string represents an integer.
        In:
            * s: The string to check.
        Out:
            * True if it is an integer in a string, False otherwise.
    """

    # Check that everything in the string is a digit
    for char in s:
        if char not in string.digits:
            return False
    return True



def is_number (s: str) -> bool:

    """
        This function verifies if a given string represents an integer or a float.
        In:
            * s: The string to check.
        Out:
            * True if it is a number in a string, False otherwise.
    """

    # If it is an integer we are done
    if is_integer(s):
        return True
    
    # Otherwise, let's check if it is a float
    i = s.find('.')
    if i != -1:
        return is_integer(s[:i]) and is_integer(s[i+1:])

    # No point found, it is something else
    return False

        

def list_of_floats (composite_list: List[Any]) -> List[float]:

    """
        This function checks the contents of the list to extract the floats inside.
        In:
            * composite_list: A list with various types.
        Out:
            * A list with the floats found in composite_list.
    """

    # Extract floats from the list
    floats_in_list = []
    for value in composite_list:
        if type(value) in (int, float) or (type(value) is str and is_number(value)):
            floats_in_list.append(float(value))
    return floats_in_list



# Test the function
if __name__ == "__main__":
    l = [1, '3.14', 2.8, [], (2,5), 'hello', {'d': 3, 'e': 2.145}]
    print(list_of_floats(l))

Use the exceptions mechanism to achieve the same result. Test on lists l and l2 = [(2, 5), 'hello', {'d': 3, 'e': 2.145}].

Solution to the exercise
# Needed imports
from typing import List, Any



def list_of_floats (composite_list: List[Any]) -> List[float]:

    """
        This function checks the contents of the list to extract the floats inside.
        In:
            * composite_list: A list with various types.
        Out:
            * A list with the floats found in composite_list.
    """

    # Extract floats from the list
    floats_in_list = []
    for value in composite_list:

        # Try some conversion and capture errors to determine type
        try:
            floats_in_list.append(float(value))
        except ValueError as e:
            print(f"ValueError: Could not convert {value} to float. {e}")
        except TypeError as e:
            print(f"TypeError: Invalid type for {value}. {e}")

    return floats_in_list



# Test the function
if __name__ == "__main__":
    l1 = [1, '3.14', 2.8, [], (2, 5), 'hello', {'d': 3, 'e': 2.145}]
    print(listOfFloats(l1))
    l2 = [(2, 5), 'hello', {'d': 3, 'e': 2.145}]
    print(listOfFloats(l2))

2 — Argmax

The argmax function returns the index of the (first occurrence of the) maximum value occuring in a list of values, if the list is empty or null (None or null), it returns -1.

Let’s write unit tests for it. Here is an example of test for the function (copy-paste it in a file tests_argmax.py), assuming argmax is in a file argmax.py:

# Needed imports
import unittest
from argmax import argmax



# Define a class for unit tests
class TestArgmax (unittest.TestCase):

    # Here, we define a method with a simple test
    # You will have to write new tests, possibly in new methods
    def test_argmax_standardcase (self):
        self.assertEqual(1, argmax([1, 5, 3, 4, 2, 0]))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestArgmax {

    @Test
    public void testArgmax1() {
        Assertions.assertEquals(1, argmax(new int[]{1,5,3,4,2,0}));
    }
}

You are going to code the function argmax(lst) and write some other tests to make sure it’s working properly. Before coding, you need to list a few unique examples that you can also use for your tests. You must also document the function and, if necessary, comment on the code.

Important

The statement unittest.main()runs the tests defined in the file. The best practice is to separate the functions and the tests. So do not forget to import the file (and functions) in the module containing the tests.

Solution to the exercise

In a file named, for instance, argmax.py:

# Needed imports
from typing import List



def argmax (lst: list[int]) -> int:
    
    """
        This function returns the index of the maximum value occurring in a list of values.
        In case of an empty list, it returns -1.
        If the maximum value occurs multiple times, the index of the first occurrence is returned.
        In:
            * lst: The list of values.
        Out:
            * The index of the first occurrence of the maximum value, or -1.
    """

    # This whole function is equivalent to:
    # return lst.index(max(lst))
    # We detail the algorithm for pedagocical purpose
    # In practice, you should use the line above

    # If not found
    if lst is None or len(lst) == 0:
        return -1
    
    # Otherwise, iterate on contents to find max
    max_val = lst[0]
    max_index = 0
    for i, v in enumerate(lst):
        if v > max_val:
            max_val = v
            max_index = i
    return max_index
/**
 * This function returns the index of the maximum value occurring in a list of values.
 * In case of an empty list, it returns -1.
 * If the maximum value occurs multiple times, the index of the first occurrence is returned.
 *
 * @param lst the list of values
 * @return the index of the first occurrence of the maximum value
 */
public int argmax(int[] lst) {
    if (lst == null || lst.length == 0) {
        return -1;
    }
    int max_val = lst[0];
    int max_index = 0;
    for (int i = 1; i < lst.length; i++) {
        if (lst[i] > max_val) {
            max_val = lst[i];
            max_index = i;
        }
    }
    // Alternatively, you can use a one-liner:
    //return Arrays.stream(lst).max().stream().findFirst().getAsInt();
    return max_index;
}

Then, in tests_argmax.py:

# Needed imports
import unittest
from argmax import argmax



# Define a class for unit tests
class TestArgmax (unittest.TestCase):

    # Here, we define a method with a simple test
    # You will have to write new tests, possibly in new methods
    def test_argmax_standardcase (self):
        self.assertEqual(1, argmax([1, 5, 3, 4, 2, 0]))
        self.assertEqual(0, argmax([8, 5]))
        self.assertEqual(1, argmax([-2, 9]))

    def test_argmax_none (self):        
        self.assertEqual(-1, argmax(None))
    
    def test_argmax_empty (self):
        self.assertEqual(-1, argmax([]))
    
    def test_argmax_multiple_max (self):
        self.assertEqual(2, argmax([1, 3, 5, 5, 2, 5]))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestArgmax {

    @Test
    public void testArgmax1() {
        Assertions.assertEquals(1, argmax(new int[]{1,5,3,5,2,0}));
    }

    @Test
    public void testArgmaxNone() {
        Assertions.assertEquals(-1, argmax(null));
    }

    @Test
    public void testArgmaxEmpty() {
        Assertions.assertEquals(-1, argmax(new int[]{}));
    }

    @Test
    public void testArgmaxMultipleMax() {
        Assertions.assertEquals(2, argmax(new int[]{1,3,5,5,2,5}));
    }
}

3 — Leap year

In this exercise, you will have to write a function is_leap(year) that takes a year as a positive integer parameter, and returns a boolean value that is true if the year is a leap year and false otherwise.

Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400 (Wikipedia).

We add an additional rule which specifies that if the integer passed as a parameter is strictly negative, then the value returned is False.

3.1 — Write the tests

Let’s first write a placeholder for the function, which is just meant for the function to be defined:

def is_leap (year: int) -> bool:

    """
        The function returns True if the input is a leap year and False otherwise.
        This function only works for positive integers.
        In:
            * year: The year to be checked.
        Out:
            * True if the year is a leap year, False otherwise.
    """

    return False
/**
 * This function takes a year as input and returns True if the year is a leap year, False otherwise.
 * An additional constraint is that the year must be greater than 0, otherwise the function will return False.
 *(assuming `is_leap` is in a file `isleap.py`)
 * @param year the year to be checked
 * @return true if year is a leap year, false otherwise
 */
public boolean isLeap(int year){
    return false;
}

Before coding the body of the function, you will write the tests. Following the principle of one test per concept, you will create the following six tests:

  1. The value returned is False for a negative value (given as an example).
  2. If the input value is odd, the value returned is False.
  3. If the input value is even and not divisible by 4, then the expected result is False.
  4. If the input value is divisible by 4 and divisible by 100, then the expected result is False.
  5. If the input value is divisible by 4 and not divisible by 100, then the expected result is True.
  6. If the input value is divisible by 400, then the expected result is True.

Here is a basic code to get started (assuming is_leap is in a file isleap.py):

# Needed imports
import unittest
from isleap import is_leap



class TestIsLeap (unittest.TestCase):

    def test_negative_value (self):
        self.assertFalse(is_leap(-1000))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)  
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestLeapYear {
    
    @Test
    public void testNegativeYear() {
        Assertions.assertFalse(isLeap(-1000));
    }

}
Solution to the exercise
# Needed imports
import unittest
from isleap import is_leap



class TestIsLeap (unittest.TestCase):

    def test_negative_value (self):
        self.assertFalse(is_leap(-1000))
    
    def test_odd_year (self):
        self.assertFalse(is_leap(1333))
        self.assertFalse(is_leap(2023))

    def test_even_not_divisivle_by_4 (self):
        self.assertFalse(is_leap(2018))
        self.assertFalse(is_leap(2026))

    def test_divisible_by_4_and_by_100 (self):
        self.assertFalse(is_leap(1900))
        self.assertFalse(is_leap(2100))

    def test_divisible_by_4_and_not_by_100 (self):
        self.assertTrue(is_leap(2008))
        self.assertTrue(is_leap(2024))

    def test_divisible_by_400 (self):
        self.assertTrue(is_leap(1600))
        self.assertTrue(is_leap(2000))



# Run the tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)  
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestLeapYear {
    
    @Test
    public void testNegativeYear() {
        Assertions.assertFalse(isLeap(-1000));
    }

    @Test
    public void testOddYear() {
        Assertions.assertFalse(isLeap(2001));
    }

    @Test
    public void testEvenYearNotDivisibleBy4() {
        Assertions.assertFalse(isLeap(2018));
        Assertions.assertFalse(isLeap(2026));
    }

    @Test
    public void testDivisibleBy4AndBy100() {
        Assertions.assertFalse(isLeap(1900));
        Assertions.assertFalse(isLeap(2100));
    }

    @Test
    public void testDivisibleBy4AndNotBy100() {
        Assertions.assertTrue(isLeap(2008));
        Assertions.assertTrue(isLeap(2024));
    }

    @Test
    public void testDivisibleBy400() {
        Assertions.assertTrue(isLeap(1600));
        Assertions.assertTrue(isLeap(2000));
    }
}

3.2 — Write the function

You can now code the logic of the function in accordance with the specifications. You need to comment out lines in a meaningful way to make the code clearer, using #.

Important

Make sure that all the tests pass!

Solution to the exercise
def is_leap (year: int) -> bool:
    
    """
        This function takes a year as input and returns True if the year is a leap year, False otherwise.
        An additional constraint is that the year must be greater than 0, otherwise the function will return False.
        In:
            * year: The year to be checked.
        Out:
            * True if year is a leap year, False otherwise.
    """
    
    if year >= 0:  
        if year % 4 == 0:
            if year % 100 == 0:
                return year % 400 == 0
                # since the year is divisible by 100, 
                # it must also be divisible by 400 to be a leap year     
            else:
                # divisible by 4 and not by 100
                return True
        else:
            # not divisible by 4
            return False
    else:
        # negative years are not leap years
        return False
/**
 * This function takes a year as input and returns true if the year is a leap year, false otherwise.
 * An additional constraint is that the year must be greater than 0, otherwise the function will return false.
 *
 * @param year the year to be checked
 * @return true if year is a leap year, false otherwise
 */
public boolean isLeap(int year) {
    if (year >= 0) {
        if (year % 4 == 0) {
            if (year % 100 == 0) {
                return year % 400 == 0;
                // since the year is divisible by 100, 
                // it must also be divisible by 400 to be a leap year     
            }else{
                // divisible by 4 and not by 100
                return true;
            }
        }else{
            // not divisible by 4
            return false;
        }
    }else{
        // negative years are not leap years
        return false;
    }
}

4 — Valid password

To be considered sufficiently secure, a password must comply with the following rules:

  1. Must contain at least 8 characters.
  2. Must contain at least one uppercase letter.
  3. Must contain at least one lowercase letter.
  4. Must contain at least one number.
  5. Must contain at least one special character (from !@#$%^&*()).

Write the function is_strong_password(password) which takes a string (a.k.a., sequence of characters) as input and returns a Boolean indicating whether the password entered is strong. To help design the logic of the function, you can break it down into sub-functions, each of which will be tested independently. A skeleton of the function is provided below, divided into sub-functions. You must also document and comment on your code.

Finally, you need to check that the rules expressed in the specifications are correctly taken into account by writing unit tests. The test class is not provided, so you will need to write it yourself. Each sub-function must be tested independently with a variety of examples. Once each sub-function is tested, you can test the main function.

This last stage can be done in pairs: each person writes the tests for the other. Then, you can evaluate the correctness of your function by running the tests. Possibly, either the function or the tests will need to be modified to ensure that the function is working properly.

Here is a file to get you started:

Solution to the exercise

5 — Valid password again

Now that you have a function that checks the strength of a password, you will document it using the docstring format. You must document the module and each of its functions with valuable information, such as the purpose of the function, the parameters it takes, and the value it returns. Such information will help other developers understand how to use the function. Extensions in VSCode exist to automatically generate the skeleton of a function docstring. Once you have documented the code, you can generate the documentation using the pydoc or javadoc tool, depending on the language you are using.

First, open a terminal and navigate to the directory where the file is located. The file must be named strongpassword.py.

Then, run the following command:

python -m pydoc -w strongpassword
python -m pydoc -w strongpassword
python3 -m pydoc -w strongpassword
python3 -m pydoc -w strongpassword

pydoc will generate an HTML file named strongpassword.html in the same directory. You can open this file in a web browser to view the documentation.

First, open a terminal and navigate to the directory where the file is located. The file must be named StrongPassword.java.

Then, run the following command:

javadoc -d doc StrongPassword.java
javadoc -d doc StrongPassword.java
javadoc -d doc StrongPassword.java
javadoc -d doc StrongPassword.java

javadoc will generate a directory named doc containing the documentation. You can open the index.html file in a web browser to view the documentation.

Once done, check the generated documentation!

6 — Optimize your solutions

What you can do now is to use AI tools such as GitHub Copilot or ChatGPT, either to generate the solution, or to improve the first solution you came up with! Try to do this for all exercises above, to see the differences with your solutions.

To go further

7 — Takuzu checker

Takuzu is a logic game where you have to complete a pre-filled $n\times n$ grid using only 0s and 1s. A grid is completed correctly if it respects the three rules:

  1. Each row and column must contain an equal number of 0s and 1s.
  2. No more than two consecutive similar numbers are allowed.
  3. Each pair of rows is different, and each pair of columns is different.

An example of a correctly completed $4\times 4$ grid:

0 1 1 0 
1 0 0 1 
0 0 1 1 
1 1 0 0 

You are asking to code a Takuzu grid checker. A module skeleton will serve as a basis for your work. You must respect the partitioning of functions and implement each of them. Of course, you will also need to create and complete a test class.

Here is a file to get you started:

  • TODO
Solution to the exercise

8 — Mastermind

Get ready to code the Mastermind game. In this game, a codemaker creates a code using 4 digits from 1-6. A codebreaker has to make up to ten guesses in order to break the code. For each guess, the codemaker gives hints to the codebreaker. Hints are made of 2 digits:

  • The first digit indicates the number of correct numbers in the correct position.
  • The second digit indicates the number of correct numbers in the wrong position.

Multiple occurrences of a value are allowed. If there are duplicate values, they cannot all get a hint unless they correspond to the same number of duplicate colours in the hidden code. For example, if the code is [6, 5, 1, 4] and the guess is [1, 1, 2, 2], the clues are (0, 1). In fact, the value 1 appears twice in the guess, but only once in the code. Since it is not in the right place, it counts as one in the wrong place. In addition, the value 2 also appears twice in the guess, but not in the code. It does not appear in the hints.

In order to progress gradually, we’ll break down the logic of the game into functions and code them as we go along. We’ll use the description provided to document the code and write tests.

We provide a partially implemented Mastermind class. Comments are provided for all methods present, whether implemented or not.

You’ll need to code the methods check_code(code), has_remaining_attempts(), is_right_answer(hints) and guess_pattern(code, guess). We recommend that you process one function after the other, writing the tests before or just after coding the function. As the last function is the most complicated, you can, if you wish, break it down into sub-functions, each of which will be tested independently.

You may need to counter values. The following data structures may be useful: from collections import Counter

Here is a file to get you started:

Solution to the exercise

To go beyond

Looks like this section is empty!

Anything you would have liked to see here? Let us know on the Discord server! Maybe we can add it quickly. Otherwise, it will help us improve the course for next year!