Exceptions and assertions

Reading time10 min

In brief

Article summary

In this course, we introduce exceptions, which indicate errors detected during program execution.

We also detail assertions, which allow to make some verifications at some point of the program. They are very practical for developement, as they allow to raise errors when something wrong happen.

Main takeaways

  • Exceptions are used to handle errors that can occur during program execution, such as invalid user input or missing files.

  • Exceptions can be caught and handled by the program.

  • The try-except block is used to catch and handle exceptions in Python.

  • The finally clause is used to define clean-up actions that must be executed under all circumstances.

  • Assertions are made to validate and enforce expected conditions in the code during runtime.

  • Failed assertions provide immediate feedback, helping developers quickly identify and fix issues.

  • Consistent use of assertions enforces constraints, leading to more robust and reliable software.

  • Assertions can be enabled/disabled easily for production.

Article contents

1 — Exceptions

1.1 — What is an exception?

Exceptions are used to deal with errors that can occur even if the code is completely correct. Here are some examples of such scenarios:

  • A user enters invalid data.
  • A file is missing.
  • A network connection is lost.

An exception is an event that occurs during the execution of a program, that disrupts the normal flow of instructions. But exceptions are not necessarily fatal, they can be handled by the program.

However, when an exception is not handled by programs, it results in error message like the one shown below:

def euclidean_division (a: int, b: int) -> int:

    """
        This function returns the integer division of a by b.
        Beware of division by 0!
        In:
            * a: The numerator.
            * b: The divisor.
        Out:
            * The integer division of a by b.
    """

    # No check for division by 0, the program will crash if b is 0
    return a // b
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This function returns the integer division of a by b.
     * Beware of division by 0!
     *
     * @param a The numerator.
     * @param b The divisor.
     * @return  The integer result of dividing a by b.
     */
    public static int euclideanDivision(int a, int b){ 
        // No check for division by 0, the program will crash if b is 0
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(euclideanDivision(10, 0));
    }

}

Running the previous code with b = 0 will raise a ZeroDivisionError in Python (ArithmeticException in Java).

Output
Traceback (most recent call last):
  File "functions.py", line 16, in <module>
    euclidean_division(10, 0) 
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "functions.py", line 14, in euclidean_division
    return a // b
           ~~^^~~
ZeroDivisionError: integer division or modulo by zero
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at org.step1.Main.euclideanDivision(Mai.java:30)
	at org.step1.Main.main(Mai.java:34)

The last line of the error message indicates what happened. Exceptions come in different types, and the type is printed as part of the message (ZeroDivisionError and ArithmeticException).
The string printed as the exception type is the name of the built-in exception that occurred.

Information

Programs can define their own exceptions by creating a new exception class (which should be derived from the Exception class), but we will not describe this in this lesson.

1.2 — Handling exceptions

1.2.1 — The try-except block

It is possible to write programs that handles selected exceptions, i.e., that detects when an exception occurs, and captures it to do something and prevent the program from crashing. To do so, we can use a try-except block to catch and handle the exception. The syntax of the try-except block is as follows :

# The code that has a chance to raise an exception should be written within the try block
try:
    <do something>

# The code to execute in case of an exception of specified type should be written in the except block
except <exception type>:
    <handle the error>

The try statement works as follows:

  1. First, the try clause (the statement(s) between the try and except keywords) is executed.
  2. Here is what can happen next:
    • If no exception occurs, the except clause is skipped, and execution of the try clause is finished.
    • If an exception occurs during execution of the try clause (e.g., at line 42), the rest of the clause (lines 43 and beyond) is skipped. Then, if the type of the exception raised matches the exception named after the except keyword (<exception type> in the code above), the except clause is executed, and then execution continues after the try-except block.
    • If an exception occurs which does not match the exception named in the except clause, it is passed on to outer try statements (if any). If no handler is found, it is an unhandled exception and execution stops with an error message.

The following example asks the user for an input until a valid integer has been entered, but allows the user to interrupt the program (using Control-C).

# Infinite loop
while True:

    # Start the block that may raise an exception
    try:

        # Here, int() can raise a ValueError if the provided string cannot be converted to an integer
        x = int(input("Please enter a number: "))

        # If we can reach this point, it means that no exception was raised
        # We abort the infinite loop
        break

    # In case of a ValueError, we make a print and loop again
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

A try statement may have more than one except clause, to specify handlers for different exceptions. At most one handler will be executed. An except clause may name multiple exceptions as a parenthesized tuple, for example:

except (RuntimeError, TypeError, NameError):
    pass

The pass statement is used as a placeholder for future code. When the pass statement is executed, nothing happens (it is a command for “do nothing”), but you avoid getting an error when empty code is not allowed. Empty code is not allowed in loops, function definitions, class definitions, or in if statements.

Information

As all Python exceptions are derived from a base class called Exception, you can catch any error that occur during the try block by writing:

except Exception:
    pass

Or, equivalently:

except:
    pass

Finally, if you want more information about the exception that you captured, you can use the traceback library, and get the exception in a variable (e in the code below):

# Needed import
import traceback

# Raise and catch an exception
try
    x = 1 / 0
except Exception as e:

    # Print a few info
    print(f"Type of the exception: {type(e).__name__}")
    print(f"Error message of the exception: {str(e)}")
    
    # Show complete traceback (which function was called, etc.)
    print("Traceback:")
    traceback.print_exc()
1.2.2 — Defining clean-up actions

The try statement has another optional clause which is intended to define clean-up actions that must be executed under all circumstances. If a finally clause is present, the finally clause will execute as the last task before the try statement completes.
For example:

try:
    while True:
        print("Program is running")

except KeyboardInterrupt:
    print("Oh! you pressed CTRL + C.")
    print("Program interrupted.")

finally:
    print("This was an important code, ran at the end.")
Output
Program is running
Program is running
...
Program is running
Program is running
Prog^Cis running
Oh! you pressed CTRL + C.
Program interrupted.
This was an important code, ran at the end.
Information

The finally clause runs whether or not the try statement produces an exception.

1.3 — Raising exceptions

Exceptions are raised when an error occurs. But it is also possible for a programmer to force an exception to occur with the keyword raise. The argument to raise indicates the exception to be raised. This must be either an exception instance or an exception class.

For example:

def euclidean_division (a: int, b: int) -> int:

    """
        This function returns the integer division of a by b.
        Beware of division by 0!
        In:
            * a: The numerator.
            * b: The divisor.
        Out:
            * The integer division of a by b.
        Exceptions:
            * ZeroDivisionError: If b is 0.
    """

    # Raise an exception with a custom error message
    if b == 0:
        raise ZeroDivisionError("Division by zero. Please provide a non-zero divisor.")
    
    # Perform a division that cannot be by 0
    return a // b
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This function returns the integer division of a by b.
     * Beware of division by 0!
     *
     * @param a The numerator.
     * @param b The divisor.
     * @return  The integer result of dividing a by b.
     * @throws ArithmeticException If b is 0.
     */
    public static int euclideanDivision(int a, int b) throws ArithmeticException {
        // Raise an exception with a custom error message
        if (b == 0) {
            throw new ArithmeticException("Division by zero. Please provide a non-zero divisor.");
        } 
        // Perform a division that cannot be by 0
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(euclideanDivision(10, 0));
    }

}

In the example above, the function euclidean_division raises a ZeroDivisionError in Python (and an ArithmeticException in Java) if the divisor b is 0. Here, the same type of error is raised but the message differs. This mainly shows how to raise an exception.

We could go one step beyond and define our own class extending Exception and raise it, instead of just customizing the error message. With this, we could add properties to the exception object created and use them in the except block.

2 — Assertions

Assertions are a crucial tool in software development used to validate assumptions made in the code. They are specific types of exceptions, made for development purpose. Assertions check whether specific conditions hold true during execution, helping to catch bugs and logical errors early.

When an assertion fails, it provides immediate feedback, aiding in quick debugging. By enforcing constraints and expected behaviors, assertions contribute to creating more reliable and maintainable code.

2.1 — What is an assertion?

Assertions give us a way to make our assumptions explicit in our code. They are used to check that the state of the program is as expected at a given point in the code. If the assertion fails, an exception is raised, and the program stops. So, the assertion mechanism is used to check conditions during code execution.

Most of the time, it does not require any additional dependency. In Python (as in Java), an assertion is added using the reserved word assert, followed by the condition to be checked, and an optional message to be printed if the assertion fails: assert condition, message.

Let’s consider the euclidean_division seen earlier. In this code, we have an assumption that b != 0. Therefore, an assumption may be more adapted than an exception:

def euclidean_division (a: int, b: int) -> int:

    """
        This function returns the integer division of a by b.
        Beware of division by 0!
        In:
            * a: The numerator.
            * b: The divisor.
        Out:
            * The integer division of a by b.
    """

    # This assertion will fail when a division by 0 is attempted
    assert b != 0, "Division by zero"

    # If we pass all assertions, we are fine
    return a // b
/**
 * To run this code, you need to have Java installed on your computer, then:
 * - Create a file named `Main.java` in a directory of your choice.
 * - Copy this code in the file.
 * - Open a terminal in the directory where the file is located.
 * - Run the command `javac Main.java` to compile the code.
 * - Run the command `java -ea Main` to execute the compiled code.
 * Note: '-ea' is an option to enable assertions in Java.
 */
public class Main {

    /**
     * This function returns the integer division of a by b.
     * Beware of division by 0!
     *
     * @param a The numerator.
     * @param b The divisor.
     * @return  The integer result of dividing a by b.
     */
    public static int euclideanDivision(int a, int b){ 
        // This assertion will fail when a division by 0 is attempted
        assert b !=0 : "Division by zero";

        // If we pass all assertions, we are fine
        return a / b;
    }

}

Assertions can be used to enrich code by checking code invariants and thus be seen as a form of documentation: they can describe the state the code expects to find before it runs.

However, assertions are only used in the development phase and are rendered silent in production. Therefore, in production, the verification that b != 0 above is not made. It is thus assumed that the program should never call the function with this problematic case.

Information

This is made to speed up the code and avoiding unnecessary checks.

However, if you feel that the error may still happen during production, an exception may still be more adapted. This is especially the case when you interact with a user that could set b = 0.

As assertions may be disabled, you should pay attention to the following points:

  • Assertions should not have side effects (they should not modify the state of the program, but only check it) – For example, you should not use assertion to open a file, update a datastructure, etc.

  • Assertions should not be used to check for conditions that can be caused by external factors (e.g., user input, network, etc.) – For example, you should not use assertions to check if a file exists.

  • Assertions are not a substitute for error handling, input validation, testing, documentation, etc.

  • When using assertions that require a pretty high execution time, make sure that computations are made in the assert condition. Indeed, if you compute the result before, store it in a variable, and then check the variable in the assert, the computation will still be done when disabling assertions. For such complex verifications, it may thus be a good idea to define functions, and call them in the assert. For instance, in the following code, the function is_sorted is called in the assert:

def binary_search (data: list[int], search_value: int) -> int :

    """ 
        This function searches for a value in a sorted list using the binary search algorithm.
        In:
            * data: The list to search in.
            * search_value: The value to search for.
        Out:
            * The index of the value in the list if it is found, -1 otherwise.
    """

    assert len(data) > 0, "The list is empty"
    assert is_sorted(data), "The list is not sorted"    

To go further

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!

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!