The Definition of assert#

The Python documentation is rather detailed, and the assert documentation is no exception. Specifically, it states that an assert statement is equivalent to:

if __debug__:
    if not expression: raise AssertionError

or the following if a second expression is given:

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

The very important part about assert is that it relies on the __debug__ flag and its current state. If Python is not in debug mode, all assert statements are ignored. Therefore, you cannot rely on assert to throw an error in all situations.

What is __debug__?#

Per the Python documentation, __debug__ is true if Python was not started with a -O option. There are two options, -O and -OO. These options do a few things. First, -O will compile Python into bytecode, which will improve speed performance. -O will also set __debug__ to False, which causes all the assert statements to be dropped. The -OO also drops docstrings.

Typically these optimization flags are used when something is being deployed to production or being distributed. This is because they will be slightly faster, and in the case of -OO will remove docstrings, which may not be desired if distributing code. As a side node, distributing Python bytecode should be considered as distributing the code because even with -O or -OO the code can still be decompiled with tools like python-uncompyle6. I’d consider this security through obscurity, which is not a good idea.

The __debug__ flag can be very useful in code if you are looking for deeper optimizations. For example, putting dense logging statements inside an if __debug__: will allow them to be fully optimized out, instead of checking the debug level of the logger, which is done at runtime.

We know if __debug__ is not set, asserts are ignored, but is there any other way to ignore fix this? Not to my knowledge, because the __debug__ documentation also notes that assignment to that value is forbidden, just like True, False, and None. Trying to do so results in a SyntaxError. Therefore, if you launch Python with -O or -OO, asserts will be removed, and there is no way to get them back.

What to do instead?#

Unless the assert is purely for debug purposes, the most common method to handle this is to throw an AssertionError. It is very easy to convert as follows:

assert len(list_input) != 0

into

if len(list_input) == 0:
    raise AssertionError("Input list cannot be zero length")

You can choose a different error, like ValueError, NameError, IndexError, or some other more specific exception type. Doing so may even help make it easier to debug with a more accurate exception.

Really Bad Assert Statements#

While in many situations, an assert is catching something simple, like the length of a list. But if the assert does any modifications, the behavior can change depending on if Python is running with -O or -OO.

For example, suppose that you want to check that a dictionary has a key, and that its value matches what is expected, but you don’t want it in the result. Maybe this dictionary represents if some result was valid.

input_message: dict[str, str] = {"sender": "Descartes", "message": "I think, therefore I am", "valid": "true"}

assert input_message.pop("valid") == "true"

After running this code, input_message looks like:

>>> input_message
{'sender': 'Descartes', 'message': 'I think, therefore I am'}

I did all of this in my Python shell. This time, I’ll do the same thing, except when I launch Python, I’ll use python3 -O. This is the resultant output:

>>> input_message: dict[str, str] = {"sender": "Descartes", "message": "I think, therefore I am", "valid": "true"}
>>> assert input_message.pop("valid") == "true"
>>> input_message
{'sender': 'Descartes', 'message': 'I think, therefore I am', 'valid': 'true'}

Since the -O flag was used when launching Python, __debug__ is set to False. Therefore, assert is completely optimized out, and the pop function is never called. So, by optimizing the code, the assert is not just removed, but also changes the behavior.

I’ve made this into a script called dictionary_pop.py on GitHub. It can easily be invoked with python3 -O ./dictionary_pop.py or python3 ./dictionary_pop.py to show what happens when the assert is/is not (respectively) optimized out.

And this is not a contrived example! The PyOpenCL library had a pull request for this exact scenario. The author hadn’t contemplated that the assert may be optimized out.

Conclusion#

The only scenario where I can picture using a raw assert is if performance is critical and the input is being generated solely from within the code (eg; no user inputs). In such a case, running in a non optimized mode for testing may enable internal bugs to be caught, while not needlessly doing the check in production. But if performance is so critical that this optimization matters, I would honestly ask why are you using Python.

My rule of thumb is that 99.9% of the time a bare assert is wrong and should instead instead be a check with the appropriate thrown exception.