Programming errors
Reading time10 minIn brief
Article summary
In this article, we analyze some errors that can occur when writing a program, and discuss the particular case of programs with random elements.
Main takeaways
-
There are two types of errors: syntactic and runtime errors.
-
Random numbers may make debugging hard, therefore setting a seed is a good practice.
-
Tools can exploit type hinting to detect potential errors.
Article contents
Programming is an incremental process, meaning that the code has to be built step-by-step, each step has to be tested before moving to the next one. Development time may be roughtly divided into three iterative phases:
- Coding.
- Debuging.
- Documenting.
And debugging definitely takes up a lot of development time. An important skill to acquire is to efficiently identify the source of bugs and to fix them. Many tools have been conceived to help you quickly determine where programming errors and bugs are located. But before discovering such specific tools, it is mandatory to learn how to interpret error messages rendered by the interpreter (resp. compiler).
The Python interpreter may detect errors during the interpretation of your code that are mainly of two types.
1 — Syntactic errors
Syntactir errors are located in the text of your code, and can be detected without executing the code (during the parsing/compilation phases). They are quite explicitly explained by the interpreter as it provides you with a description of the error and the line where it occurs.
Consider the following program:
# The string to manipulate
s = "IMT Atlantique"
print(s)
# Loop to append to a new string
result = ""
for i j in range(len(s)):
result = s[i] + result
# Print the result
print("Reversed string:", result)
/**
* In Java, the entry point of a program is the 'main' function, in the 'Main' class, in a file named 'Main.java'.
* To enable asserts, you should compile your program with `javac Main.java`.
* Then, run it with the command `java -ea Main`.
*/
public class Main
{
/**
* This is the entry point of your program.
* It contains the first codes that are going to be executed.
*
* @param args Command line arguments received.
*/
public static void main (String[] args)
{
// The string to manipulate
String s = "IMT Atlantique";
System.out.println(s);
// Loop to append to a new string
String result = "";
for (int i j = 0; i < s.length(); i++)
{
result = s.charAt(i) + result;
}
// Print the result
System.out.println("Reversed string: " + result);
}
}
Here, the interpreter tells us that at line 7 of your code, the token j
is unexepected at this position, as it cannot follow another token corresponding to variable name (i.e., i
):
File "session_1.py", line 7
for i j in range(len(s)):
^
SyntaxError: invalid syntax
2 — Runtime errors
Runtime errors appear only when the program is running (during the execution phase). They are more tricky to solve because they often depend on the context of execution. Here are a few examples that can cause such error:
- An unauthorized operation considering the actual types of the concerned values.
- An input/output operation on an unavailable resource (e.g., a file).
- An access to unavailable area of the memory.
Consider the following program:
# Ask user at runtime for a value
den = input("Please enter the value of the denominator")
# Perform a computation
result = 42 / den
print(result)
// Needed imports
import java.util.Scanner;
/**
* In Java, the entry point of a program is the 'main' function, in the 'Main' class, in a file named 'Main.java'.
* To enable asserts, you should compile your program with `javac Main.java`.
* Then, run it with the command `java -ea Main`.
*/
public class Main
{
/**
* This is the entry point of your program.
* It contains the first codes that are going to be executed.
*
* @param args Command line arguments received.
*/
public static void main (String[] args)
{
// Ask user at runtime for a value
Scanner scanner = new Scanner(System.in);
System.out.print("Please enter the value of the denominator: ");
String denStr = scanner.nextLine();
double den = Double.parseDouble(denStr);
// Perform a computation
double result = 42 / den;
System.out.println(result);
}
}
Although syntactically correct, the third line of the following code will generate a division by zero runtime error if 0
is assigned to den
.
3 — The case of random numbers
Sometimes, programs make use of random numbers. Consider for instance the following program:
# Needed imports
import random
# Fill a list with 10 random numbers
numbers = [random.randint(-10, 10) for i in range(10)]
# Generate an error if 0 is in the list
if 0 in numbers:
raise ValueError("Zero is in the list")
# Otherwise, print the list
print(numbers)
// Needed imports
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* In Java, the entry point of a program is the 'main' function, in the 'Main' class, in a file named 'Main.java'.
* To enable asserts, you should compile your program with `javac Main.java`.
* Then, run it with the command `java -ea Main`.
*/
public class Main
{
/**
* This is the entry point of your program.
* It contains the first codes that are going to be executed.
*
* @param args Command line arguments received.
*/
public static void main (String[] args)
{
// Create a random number generator
Random random = new Random();
// Fill a list with 10 random numbers
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 10; i++)
{
numbers.add(random.nextInt(11) - 10);
}
// Generate an error if 0 is in the list
if (numbers.contains(0))
{
throw new IllegalArgumentException("Zero is in the list");
}
// Otherwise, print the list
System.out.println(numbers);
}
}
Sometimes, when running your program, you will get the following output:
[3, 3, -2, -4, 5, 4, -5, -2, 3, 3]
Or sometimes:
[-4, 2, -6, -10, -7, -7, 3, 3, 5, 4]
Or:
Traceback (most recent call last):
File "session_1.py", line 9, in <module>
raise ValueError("Zero is in the list")
ValueError: Zero is in the list
Well, that’s expected. However, imagine a more complex scenario where the toy error above is a really important error you need to avoid. It is going to be pretty hard to debug, as sometimes your program will work as expected, and sometimes it will crash due to a semantic error.
In practice, it is possible to force a series of random numbers to always be the same. Indeed, random numbers are in fact pseudo-random. They are generated using a deterministic series that has good randomness properties, which is initialized using a “seed”, i.e., a first initial value. If the seed is known, then, the entire series can be predicted. Even better, if the seed can be set to an arbitrary value, then we can enforce that all executions of the same program will lead to the same generation of random numbers.
Each random number generator provides a way to set the seed to a given value (here, 42
).
This can be done as follows:
# Needed imports
import random
# Set the seed
random.seed(42)
# Fill a list with 10 random numbers
numbers = [random.randint(-10, 10) for i in range(10)]
# Generate an error if 0 is in the list
if 0 in numbers:
raise ValueError("Zero is in the list")
# Otherwise, print the list
print(numbers)
// Needed imports
import java.util.Random;
/**
* In Java, the entry point of a program is the 'main' function, in the 'Main' class, in a file named 'Main.java'.
* To enable asserts, you should compile your program with `javac Main.java`.
* Then, run it with the command `java -ea Main`.
*/
public class Main
{
/**
* This is the entry point of your program.
* It contains the first codes that are going to be executed.
*
* @param args Command line arguments received.
*/
public static void main (String[] args)
{
// Create a random number generator
Random random = new Random();
// Set the seed
random.setSeed(42);
// Fill a list with 10 random numbers
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 10; i++)
{
numbers.add(random.nextInt(11) - 10);
}
// Generate an error if 0 is in the list
if (numbers.contains(0))
{
throw new IllegalArgumentException("Zero is in the list");
}
// Otherwise, print the list
System.out.println(numbers);
}
}
Let’s run our program three times again
[10, -7, -10, -2, -3, -3, -6, -7, 7, -8]
[10, -7, -10, -2, -3, -3, -6, -7, 7, -8]
[10, -7, -10, -2, -3, -3, -6, -7, 7, -8]
As you see, the result is always the same. Now, what you can do to debug your progam is to find a value of the seed that leads to an error. Then, keep that seed set and try to see what happens with this particular problematic random configuration!
A few important remarks:
-
The list you obtain may be different from a computer to another, as the random number generator used may be different. Setting the seed only guarantees reproducibility on a single machine.
-
Each random number generator comes with its own seed. If you use functions from library
random
andnumpy.random
for instance, you will have to set the seeds of the two generators to guarantee reproducibility. -
You cannot use the random seed to change the performance of your programs. For instance, if your algorithm that incorporates random elements solves a problem better with
seed = 42
than it does forseed = 0
, it is unfair to report performance obtained for the first case. Indeed, it just means that42
leads to a lucky initialization of your algorithm for this particular problem instance, but it will not be the case in general. Reporting performance for random algorithms should always be done by running the program numerous time and providing averages and confidence intervals!
To go further
The content of this section is optional. It contains additional material for you to consolidate your understanding of the current topic.
4 — Type checking using MyPy
Type errors are discovered at runtime and are sometimes hidden in nested conditional blocks.
mypy
is a useful tool to check the adequacy of the types of the arguments passed to yout function.
mypy
leverages the typing hints in your code to check the correctness of the values passed as arguments of functions.
Let us consider the following code, in a file named distances.py
.
# Needed imports
from typing import Tuple
def euclidean_distance (point_1 : Tuple[float, float], point_2 : Tuple[float, float]) -> float:
"""
Compute the Euclidean distance between two 2D points.
In:
* point_1: The first point.
* point_2: The second point.
Out:
* The Eucldiean distance between the points.
"""
# Compute distance
return ((point_1[0] - point_2[0])**2 + (point_1[1] - point_2[1])**2)**0.5
# Ask for user input
x : int = input("x-value of pt")
y : int = input("y-value of pt")
pt : Tuple[float, float] = (x, y)
print(euclidean_distance((0, 0), pt))
Calling mypy distances.py
leads to the following errors clearly indicating type issues:
distances.py:23: error: Incompatible types in assignment (expression has type "str", variable has type "int")
distances.py:24: error: Incompatible types in assignment (expression has type "str", variable has type "int")
Found 2 errors in 1 file (checked 1 source file)
To go beyond
The content of this section is very optional. We suggest you directions to explore if you wish to go deeper in the current topic.
-
This is a formal way of describing a program’s behavior to show its correctness.
-
Mathematical techniques to help with implementation of program.
-
Mathematical tools to verify the correctness of a program.
-
A programming paradigm relying on notions above to design software.