Instructor Notes

Course Design


This course aims to provide “food for thought” about writing good code without getting lost in software development best practices. The idea is that when writing code, novice developers don’t focus on code structure or readability and, therefore, write code that is difficult to maintain and use. The risk is to write code that is only used once. By providing some guidance and simple consideration, code can improve substantially in terms of readability, making it more sustainable.

There are a few misconceptions that this course aims to correct:

  • “I will always remember my code”: The idea is that since I wrote the code, I will always be able to explain it to others. The reality is different. When a code is completed, used once or for a limited time, even the author will forget how it works or what it does. Therefore, when it is needed to reuse it in the future, a lot of time is wasted trying to understand it. If the code is written, keeping in mind that we need to reuse it, we can write better code that requires less time to read.

  • “Code is for the computer to run”. True, but also for people to read. Code is often more read than written. We read code when trying to modify it or add new features. If someone else wrote the codebase, we spent more time reading it and trying to understand how it works than actually writing new code to extend it. Creating a code for people will make it easy to read and maintain. This results in spending less time trying to understand the code.

  • “Software engineers write good code. I am a scientist, I don’t need to”. Scientists use code for their research, and even if it is not their main outcome, code is sometimes the main tool to reproduce the results. Often, they share their code with a large community so that other people can use it. If the code is not clear, not tested and not easy to extend, it cannot be easily used. Therefore, scientists must invest time in writing good-quality code.

  • “I don’t have time for testing”. Testing is the most underrated task in any coding activity. Design and writing tests require time, and although some testing is added to evaluate the code, coding projects often lack a reliable testing framework. The truth is testing can actually save time in the long run. Every time a code needs to be improved or extended, a test will be required to ensure that any change did not affect previous functionality. Testing also provides trust in the code and, therefore, in the results it produces.

Course structure


The course is divided in four episodes:

  • Episode 1: Write Readable Code
  • Episode 2: Write Elegant Code
  • Episode 3: Write Graceful Code
  • Episode 4: Write Reliable Code

The first two episodes are designed to be short and don’t requires a lot of live coding. They are more for discussion, to let the students think about what can be changes to improve readability of the code. Episode 3 explains how the handle errors in the code and why it is important to add docstrings in the code. Episode 4 requires more time, as it introduce Pytest functionality. The idea of this last episode is to introduce unit tests in the development pipeline. This last episode builds on the previous ones. It incorporates concept like input validation, docstring naming conventions, and it can run as stand alone lesson.

Course delivery


Although not particularly endorsed, this code works better in VSCode. This is because some of the Vscode extensions can be introduced to facilitate the creation of the docstrings or introduce pylint (extra components that can be added to the course).

The ideal cohort is 5 students. This is because part of the course is based on discussion about the code, and students in smaller classes might find it easier to participate in the discussion. For a larger cohort, it is advisable to have at least 1 helper for four students and break the class into rooms to facilitate discussion.

Write Readable Code


Instructor Note

Ask the learners about what they think about the following code. The point is to make them read it and tell how easy/difficult it is. The function visualizes a spiral, where the radius of each point increases linearly from the centre outward, and the points are positioned according to their respective angles in the given input array. To guide the discussion, use some of the following questions What do you think this code does? - Can you use it? - What do r, x, and y represent in this code? Are these names intuitive? What to point out: - There is no information about the input parameter: should it be a list? A single value? - The names of the function, variables are poorly chosen. - The function does not handle the output well. It creates points in polar coordinates, but it does not return them. It shows the plot without returning it or allowing for extra steps. d To run the function (if necessary):

PYTHON

t = np.linspace(0,4*np.pi, 1000)
f(t)

Suggestion: Take note of the main points you might need for the following discussions.



Instructor Note

Require Live coding The aim of the above discussion is to modify and improve the code together, focusing on the points identified in the previous discussion. The simpler solution might be just to focus on the names and add some comments.

PYTHON

#Plot a spiral in polar coordianate
#for a given list of theta values

def plot_spiral(theta):
  # Creates an array of evenly spaced radius values from 0 to 1. 
  # The final array has the same number of element than theta
  radius = np.linspace(0,1,len(theta)) 
  # Converts polar coordinates to Cartesian
  x_coordinate = radius * np.cos(theta)
  y_coordinate = radius * np.sin(theta)
  plt.plot(x_coordinate,y_coordinate)
  plt.show()

The point is to show that simple modification can improve code readability.

A better version can be:

PYTHON

import numpy as np
import matplotlib.pyplot as plt

# Calculate polar coordinate for an array of angles.

def calculate_polarcoordinate(theta): #Meaningful names: changed names of the function and input argument

  # Creates an array of evenly spaced radius values from 0 to 1. 
  # The final array has the same number of element than theta
  radius = np.linspace(0,1,len(theta))
  # Converts polar coordinates to Cartesian
  x_coordinate = radius * np.cos(theta)
  y_coordinate = radius * np.sin(theta)
  
  return x_coordinate, y_coordinate #Better output handling

#plot a spiral in polar coordinate
def visualise_spiral_polar_coordinates(x_coordinate,y_coordinate): #split data and visualisation

  plt.plot(x_coordinate, y_coordinate)
  plt.xlabel('x')
  plt.ylabel('y')
  plt.title('Spiral in Polar Coordinates')
  plt.show() 

Focus on the change:

  • Better variable and function names make the code self-explanatory
  • Separation of Concern: The original function has been split in two so that it is clear what happens
  • Let them focus on the fact that the radius definition depends on two magic numbers, 0 and 1.
  • This example can be used to introduce the DRY and SoC principles.


Write Elegant Code


Instructor Note

The variable defines a relative velocity; the rlt stands for relative. Try to spell the v_lrt aloud to show how difficult it is.



Write Robust Code


Write Reliable Code


Instructor Note

Point to the use of sides+1 in the code. This ensures the random numbers are generated within [1, sides] (inclusive).



Instructor Note

The above challenge is designed as a pure exercise to check student understanding. It can be used to discuss the similarity with the previous code and to introduce the pytest.mark.parametrize.



Instructor Note

Highlight the fact that the docstrings should change as the code changes.



Instructor Note

Take some time to read and explain the code to the students. Things to focus on: - The EXPECTED_AVERAGE is calculated for a six-sided die. The code can be extended to test other values, for example, by combining it with pytest.mark.parametrize:

PYTHON

@Pytest.mark.parametrize("sides, num_rolls", [
    (6, 10000),(10, 10000)
])
def test_average_roll(sides, num_rolls):
    """Test the die is unbiased. 
    Assert that the average score over a number of rolls is equal to the expected average for different dice.
    """
    average = calculate_average_rolls(sides, num_rolls)
    EXPECTED_AVERAGE = sum(range(1,sides+1)) / sides
    assert  round(average,1) == pytest.approx(EXPECTED_AVERAGE)
  • The use of round is to fix approximation errors, especially when the number of rolls is small.
  • Discuss that the test should not take too long to run. So using a very large number of rolls can provide better results, but it might also take too long to run. For real-life case, you want to balance execution time and accuracy.