28 How to Reasonably Utilize Assert

28 How to Reasonably Utilize Assert #

Hello, I’m Jingxiao.

I believe that in your daily coding, you must have seen assert more or less. I have also been asked by some colleagues to add assert statements during code reviews to make the code more robust.

However, despite this, I found that in many cases, assert is easily overlooked and people don’t seem to care about this seemingly “insignificant” thing. But in fact, if used properly, this seemingly “insignificant” thing can be very beneficial to our programs.

Having said that, what exactly is assert, and how should we use assert properly? In today’s lesson, I will show you how to use it.

What is assert? #

The assert statement in Python is a good tool for debugging, mainly used to test if a condition is satisfied. If the test condition is satisfied, nothing happens, similar to executing the pass statement. If the test condition is not satisfied, it raises an AssertionError exception and returns the specific error message (optional).

Its syntax is as follows:

assert_stmt ::= "assert" expression ["," expression]

First, let’s look at a simple form of assert expression, for example:

assert 1 == 2

It is equivalent to the following two lines of code:

if __debug__:
    if not expression: raise AssertionError

Now let’s look at the form assert expression1, expression2, for example:

assert 1 == 2, 'assertion is wrong'

It is equivalent to the following two lines of code:

if __debug__:
    if not expression1: raise AssertionError(expression2)

Here, __debug__ is a constant. If the Python program is executed with the -O option, for example, Python test.py -O, all assert statements in the program will be disabled and the constant __debug__ will be False; otherwise, __debug__ will be True.

However, it is important to note that assigning a value directly to the constant __debug__ is illegal because its value is determined when the interpreter starts running and cannot be changed in the middle.

Furthermore, always remember not to use parentheses when using assert, for example:

assert(1 == 2, 'This should fail')
# Output
<ipython-input-8-2c057bd7fe24>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
  assert(1 == 2, 'This should fail')

If you write it like this, regardless of whether the expression is correct (such as 1 == 2, which is obviously incorrect), the assert check will never fail, and the program will only give you a SyntaxWarning.

The correct way to write it should be without parentheses, like this:

assert 1 == 2, 'This should fail'
# Output
AssertionError: This should fail

In summary, the purpose of assert in a program is to do some internal self-checking of the code. Using assert means that you are confident that this condition will definitely occur or definitely not occur.

For example, suppose you have a function and one of its parameters is a person’s gender. Since gender is only male or female (referring to biological gender here), you can use assert to prevent illegal input to the program. If your program has no bugs, the assert will never raise an exception. But once it raises an exception, you know there is a problem with the program, and you can easily locate the source of the error based on the error message.

Usage of assert #

After introducing the basic syntax and concepts of assert, let’s look at some practical examples to understand how assert is used in Python and clarify its use cases.

In the first example, let’s say you are working on a promotion campaign for some columns at Geek Time. You need to write a function called apply_discount() which takes the original price and the discount as input, and returns the discounted price. The function can be written as follows:

def apply_discount(price, discount):
    updated_price = price * (1 - discount)
    assert 0 <= updated_price <= price, 'price should be greater or equal to 0 and less or equal to original price'
    return updated_price

As you can see, after calculating the updated price, we have included an assert statement to check if the updated price is greater than or equal to 0 and less than or equal to the original price. If this condition is not met, an exception will be raised.

We can try inputting some values to test this functionality:

apply_discount(100, 0.2)
Output: 80.0

apply_discount(100, 2)
Output: AssertionError: price should be greater or equal to 0 and less or equal to original price

As you can see, when the discount is 0.2, the output is 80 and there are no issues. But when the discount is 2, the program throws the following exception:

AssertionError: price should be greater or equal to 0 and less or equal to original price

In this way, if a developer modifies the code related to this functionality or adds new features that result in an abnormal discount value, any issues can be easily detected during testing. As mentioned earlier, the addition of assert can effectively prevent bugs and improve the robustness of the program.

Let’s consider another example, which is the most common division operation encountered in any field of computation. Again, let’s use Geek Time as an example. Suppose Geek Time wants to know the average selling price of each column, given the total sales and the number of sales. The average selling price can be easily calculated as follows:

def calculate_average_price(total_sales, num_sales):
    assert num_sales > 0, 'number of sales should be greater than 0'
    return total_sales / num_sales

Similarly, we have included an assert statement to specify that the number of sales must be greater than 0, which prevents the calculation of prices for columns that have not yet been sold.

In addition to these two examples, assert has some other common use cases in practical work, such as the following scenario:

def func(input):
    assert isinstance(input, list), 'input must be type of list'
    # The operations below are based on the premise that input must be a list
    if len(input) == 1:
        ...
    elif len(input) == 2:
        ...
    else:
        ...

In this case, all the operations within the func() function are based on the premise that the input must be a list. This requirement might sound familiar, so it is necessary to add an assert check at the beginning to prevent errors in the program.

However, it is important to analyze based on the specific situation. For example, the reason why we can use assert in the example above is because we are certain that the input must be a list and cannot be of any other data type.

If your program allows input of other data types and has different processing methods for different data types, then you should use if-else conditional statements instead:

def func(input):
    if isinstance(input, list):
        ...
    else:
        ...
}

Example of assert error #

Earlier, we talked about many use cases of assert, which might give you the wrong impression or confuse you: can we replace many if condition statements with assert? This kind of thinking is not accurate. Next, let’s look at a few typical incorrect usages together to avoid some assumptions.

Let’s continue using GeekTime as an example. Let’s assume the following scenario: sometimes, the backend needs to delete some columns that have been online for a long time. So the relevant developers designed the following column deletion function:

def delete_course(user, course_id):
    assert user_is_admin(user), 'user must be admin'
    assert course_exist(course_id), 'course id must exist'
    delete(course_id)

GeekTime’s rule is that only admins can delete columns, and the column being deleted must exist. Some students might think this requirement is very familiar, so they added the corresponding assert checks at the beginning. Now, I want you to think about whether this way of writing is correct or not?

The answer is obviously negative. You may think that from the perspective of the functionality of the code, there is nothing wrong with it. But in actual engineering, basically no one writes code like this. Why?

Please note that I mentioned earlier that assert checks can be disabled. For example, when running a Python program, adding the -O option will disable assert checks. Therefore, once assert checks are disabled, the functions user_is_admin() and course_exist() will not be executed. This will lead to:

  • Any user has permission to delete columns.
  • Regardless of whether the course exists or not, they can force the deletion operation.

This obviously brings significant security vulnerabilities to the program. Therefore, the correct approach is to use condition statements for the corresponding checks and throw exceptions appropriately:

def delete_course(user, course_id):
    if not user_is_admin(user):
        raise Exception('user must be admin')
    if not course_exist(course_id):
        raise Exception('course id must exist')
    delete(course_id)

Let’s look at another example. If you want to open a file and perform a series of operations such as data reading and processing, the following incorrect approach is obviously not correct:

def read_and_process(path):
    assert file_exist(path), 'file must exist'
    with open(path) as f:
        ...

Because the use of assert indicates that you forcefully specify that the file must exist, but in reality, this assumption is not always true. In addition, opening a file operation may also trigger other exceptions. Therefore, the correct approach is to handle exceptions using try and except:

def read_and_process(path):
    try:
        with open(path) as f:
            ...
    except Exception as e:
        ...

In summary, assert is not suitable for runtime error checks. For example, if you try to open a file that does not exist, or if you try to download something from the Internet but the network disconnects halfway, you should still refer to the content of “Errors and Exceptions” we discussed earlier to handle them correctly.

Summary #

In today’s class, we learned about the usage of assert. Assert is usually used to perform necessary self checks on the code, indicating that you are certain that a certain situation will definitely occur or will definitely not occur. It is important to note that when using assert, do not include parentheses, otherwise the assert check will never fail regardless of the correctness of the expression. Additionally, assert statements in the program can be globally disabled using options such as -O.

Through the usage scenarios we covered in this class, you can see that the reasonable use of assert can increase the robustness of the code and also facilitate developers in locating and troubleshooting errors.

However, we should not misuse assert either. In many cases, the different situations that occur in the program are expected, and we need to handle them with different approaches, making conditional statements more appropriate. For run-time errors in the program, please remember to use exception handling.

Reflection Questions #

Finally, let me leave you with a reflection question. In your daily work and study, have you used assert before? If so, in what situations did you use it? Have you encountered any problems?

Feel free to share your experiences, insights, and doubts in the comments section. You are also welcome to share this article with your colleagues and friends. Let’s communicate and progress together.