Writing sanity checks is a practice as old as programming itself.
These checks involve checking whether certain assumptions, such as calculations, remain valid as a developer continues to build their program. If an assertion becomes false, it indicates to the developer(s) that there's a bug in the program.
These tests are more formally known as assertions. While they are primarily used for debugging code, they are also helpful in testing and documenting programs. Post debugging and testing, the assertions are turned off to optimize the program for production.
All in all, assertions help developers make their programs more reliable and efficient. This guide will walk you through using the assert keyword to write sanity checks in Python.
What Are Assertions?
An assertion helps you test the validity of your program by testing whether some conditions remain true. As you can guess, this can be extremely helpful during debugging.
The idea is that an assertion remains true as long as there are no bugs in the code. If the condition you set becomes false, the assertion will raise an exception and terminate the program.
For instance, you can test a condition that explains, "this return value is a string," or another like, "this argument isn't None." If these statements become false, you'll see an exception and know that there's a bug in your code.
Testing your code this way allows you to find errors as soon as they occur and in the development phase itself.
What Can You Do with Assertions?
Assertions help ensure that no new bugs are introduced into your code when you're adding new features to your program or fixing existing bugs.
You can also use assertions to test the preconditions and postconditions in your code. A precondition is an assumption about the input at the beginning of a function. A postcondition is an assumption about the output – the return values – of a function. Assertions can check the validity of both these types of assumptions.
The nice thing about assertions in Python is that developers can include a message to clearly describe the error that has occurred. Of course, including such messages is optional but considered best practice. That said, this feature makes assertions an excellent means of documenting the ins and outs of code.
Comments and docstrings often passively indicate the workings of code. However, assertions enable developers to take action immediately since the error message clarifies what needs to be done.
Another helpful use case of assertions is writing test cases. You can write straightforward test cases that quickly let you know whether a condition is met.
What Assertions are Not Good For
Using assertions for processing or validating data isn't a good idea since assertions are removed during the production phase. When this happens, all the processing and validation done via assertions disappear.
This is a common pitfall every developer must avoid. Another pitfall to avoid is using assertions for error handling. Specifically, you must avoid writing try … except statements that catch assertions errors. Error handling is simply not the purpose of assertions.
The Basics of the Python assert Keyword
In Python, assert is a keyword and not a method – and it's best to make a note of it right when you start learning about it. Many confuse assert for being a method that causes issues of all kinds.
With the fair warning out of the way, let's look at the assert's syntax. Besides the keyword itself, an assert statement comprises the condition and an optional message.
The condition is expected to remain true always, and as long as it is, the assert statement doesn't stop execution. But if the condition turns false, the statement stops the program and raises the AssertionError.
This is what an assert statement looks like:
assert expression[, assertion_message]
The "expression" in the statement above is a placeholder for both expressions and objects. As we discussed, the expression is tested for truthiness. Though the assertion_message argument is optional, it's a good idea to use it. It holds a string that must describe the error the assert statement is intended to catch.
Let's see a simple real-life example of the assert statement:
>>> value = 21 >>> assert value > 0 >>> value = -21 >>> assert value > 0 Traceback (most recent call last): ... AssertionError
In the first two statements, we see how the assertion remains true, and the execution doesn't stop. However, in the next two statements, we see how "value" is changed, and the assert statement stops execution right away. The AssertionError exception appears.
This same code can be made a lot clearer using the assertion_message argument:
>>> value = 21 >>> assert value > 0, f"the value is expected to be greater than 0, the value is: {value}" >>> value = -21 >>> assert value > 0, f"the value is expected to be greater than 0, the value is: {value}" Traceback (most recent call last): ... AssertionError: the value is expected to be greater than 0, the value is: -21
Now, our assertion message unambiguously describes what the condition is and why the condition failed.
It's noteworthy that you don't need to use parenthesis to group the expression and the message parameter since assert is a keyword and not a method.
In fact, using the parenthesis can result in unexpected outcomes. Here's an example of this:
>>> value = 21 >>> assert(value > 0, f"the value is expected to be greater than 0, the value is: {value}") <stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
Newer versions of Python will alert you if the syntax seems odd. But in older versions, the SyntaxWarning doesn't appear, and the same statement would simply succeed whenever it runs.
You will likely encounter this issue when you code long expressions or messages that take up more than one line. Using parenthesis is natural in these circumstances. But the issue is that using parenthesis splits the expression and message into a two-item tuple.
To prevent the SyntaxWarning from appearing, you can use a backslash character to join the lines explicitly. Here's what that looks like:
value = 21 assert ( value > 0 and isinstance(value, int), \ f"the value is expected to be greater than 0, the value is: {value}" )
The backslash at the end of the first statement inside the parenthesis joins the two physical lines of the assertion, making it a single logical line. This way, you can write longer messages while preventing logical errors and warnings.
Interestingly, there's also an edge case of this parenthesis issue. If you don't use the message parameter but still use parenthesis, there won't be any issues in your code:
>>> value = 21 >>> assert(number > 0) >>> value = -21 >>> assert(number > 0) Traceback (most recent call last): ... AssertionError
But why?
Python needs a comma after the item to consider something a single-item tuple. The parenthesis doesn't create a tuple – the comma does. So, since there is no comma in the code above, there is no tuple and no SyntaxWarning.
Still, even if using parenthesis doesn't cause issues, it's not recommended that you use them. It might cause you issues later down the road.
The AssertionError Exception
If the condition you pass in the expression argument evaluates to false, the assert statement will throw an AssertionError. This will happen whether you write the optional message or not.
When you use assert in your daily programming, you'll rarely raise the AssertionError explicitly. This is because assert raises this exception of its own accord when the specified condition fails.
AssertionError is a built-in exception, inheriting its characteristics from the Exception class in Python. It is called a concrete exception that you must raise and not subclass.
Common Ways of Using the assert Keyword
Now that you understand the basics of the assert statement let's explore the common ways it is used. There are multiple assertion formats, and knowing the ones used most frequently will help implement better assertions.
#1 Comparison Assertions
Comparison assertions are some of the most commonly used assertions. Here's an example of one:
>>> assert 6 > 4 >>> assert 6 == 4 Traceback (most recent call last): ... AssertionError >>> assert 6 > 4 and 9 < 14 >>> assert 51 == 23 or 60 > 34 Traceback (most recent call last): ... AssertionError
As you can see, these assertions involve testing conditions that compare at least two objects. The use of comparison operators is involved, which gives this type of assertion its name. Comparison assertions may also involve the use of compound expressions using Boolean operators.
#2 Membership Test
A second format you will often see used with the assert keyword is the membership test. Here's what these look like:
>>> values = [15, 21, 6, 78, 91] >>> assert 6 in values >>> assert 21 in values Traceback (most recent call last): ... AssertionError
This type of assertion enables checking whether the supplied item is present in a collection. You can use this assertion with any type of collection, including tuples and dictionaries. You will need to use the membership operators "not in" and "in" in your membership assertions.
There are two other types of assertions you should know about.
#3 Identity Assertions
As you might have guessed, this type of assertion involves testing the identities of various objects, like so:
>>> value1 = 1 >>> value2 = value1 >>> value3 = None >>> assert value1 is value2 >>> assert value1 is not value2 Traceback (most recent call last): ... AssertionError >>> assert value3 is None >>> assert value3 is not None Traceback (most recent call last): ... AssertionError
Just like membership tests, you will need to use the "is" and "is not" operators (the identity operators) to test the object's identity.
#4 Type Check Assertions
A type check assertion typically involves using the isinstance() method to ensure that the supplied object is an instance of the supplied class or classes. Here's an example:
>>> value = 33 >>> assert isinstance(value, int) >>> value = 33.0 >>> assert isinstance(value, int) Traceback (most recent call last): ... AssertionError
There are several other possibilities for using assert. For instance, one noteworthy use case involves using the all() and any() methods to write assertions. These test the truthiness of values in an iterable:
>>> assert all([True, True]) >>> assert all([True, False]) Traceback (most recent call last): ... AssertionError >>> assert any([True, False]) >>> assert any([False, False]) Traceback (most recent call last): ... AssertionError
So, the assertions with the all() method test whether all elements of an iterable are true. In contrast, the assertions using the any() method test whether any item in an iterable is true.
Conclusion
Now that you understand how the assert keyword works, you're ready to write more efficient programs that are also more reliable.