Exceptions and assertions
Reading time10 minIn 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).
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.
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:
- First, the
try
clause (the statement(s) between thetry
andexcept
keywords) is executed. - Here is what can happen next:
- If no exception occurs, the
except
clause is skipped, and execution of thetry
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 theexcept
keyword (<exception type>
in the code above), theexcept
clause is executed, and then execution continues after thetry
-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.
- If no exception occurs, the
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.
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.")
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.
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.
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 theassert
, 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 theassert
. For instance, in the following code, the functionis_sorted
is called in theassert
:
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!