Python Assertions, or Checking If a Cat Is a Dog

Author:Murphy  |  View: 28802  |  Time: 2025-03-23 19:34:01

PYTHON PROGRAMMING

A false assertion should stop you: something is wrong! Photo by Jose Aragones on Unsplash

An assertion is a statement you can use to test an assumption about your program. This short definition is, on the one hand, clear. On the other hand, it's far from explaining when you should use assertions.

The assert statement, which is the main assertion tool in Python, is strictly related to the built-in __debug__ object. At some point of my Python learning, I was unaware of this object, and so I guess many data scientists and Python developers are not aware of it, either. After reading this article, you will know how to use __debug__ and assertions – and how not to use them.


The main locations in which you will find assertions are tests. Whichever testing framework you use, it uses assertions. While unittest uses dedicated methods for assertions of specific types (like .AssertTrue(), .AssertFalse(), .AssertEqual()), pytest prefers the bare assert statement. Personally, I like the simplicity of the latter. If you want to assert that, say, x is 10, you can do it in this simple way:

assert x == 10

When you want to assert that x is an integer, you can do it as follows:

assert isinstance(x, int)

For me, this is simple and clear, and being simple and being clear are two important virtues of Python code. Testing is no exception.

When a condition is not true, the assert statement raises AssertionError:

>>> x = 20
>>> assert x == 10
Traceback (most recent call last):
  File "", line 1, in 
AssertionError

You can also use an optional message:

>>> x = 20
>>> assert x == 10, "x is not 10"
Traceback (most recent call last):
  File "", line 1, in 
AssertionError: x is not 10

Now, why can't you do that in an if block, so without employing assert? It would go as follows:

>>> x = 20
>>> if x != 10:
...     raise AssertionError("x is not 10")
Traceback (most recent call last):
  File "", line 1, in 
AssertionError: x is not 10

You do something like that if you want your program to raise an exception when x is not 10. But this should not be AssertionError, because AssertionError is a specific type of error to be used in specific situations. There is more to it, however.

As Mark Lutz writes in his fantastic book Learning Python (5th ed.), the assert statement is just a shortcut. In our above example, we can write the assertion in two equivalent ways (let's use the optional message); the first one is that which you already know:

assert x == 10, "x is not 10"

This is a shortcut for a longer piece of code:

if __debug__:
    if x != 10:
        raise AssertionError("x is not 10")

Compare it to the previous if block. They do differ – and the obvious question that asks itself is, what is __debug__? An answer to this very question will help us understand what an assertion is and when to use it.

What is debug?

The __debug__ object is a Boolean variable, directly available in your Python session:

A screenshot from Python 3.11 session in the debug mode. Image by author

Note that you cannot change it inside your Python session:

A screenshot from Python 3.11 session in the debug mode. Image by author

I will show in a second how you can change __debug__ to False. First, however, let's see how __debug__ is related to assertions:

A screenshot from Python 3.11 session in the debug mode. Image by author

As you see, when you run your Python in the debug mode – and this is the default mode – __debug__ is True and assertions work in a regular way.

Let's open a Python REPL in the production mode, to see how assertions work then. To do that, we need to provide a -O flag:

A screenshot from Python 3.11 session in the production mode. Image by author

As you see, __debug__ is now False, meaning we work in the production mode. This means the code will be optimized:

  • When __debug__ is True, all assertions and whatever else follows the if __debug__: checks (which I will hereafter call debug-mode checks) will be executed.
  • When __debug__ is False, the code is optimized, in a way that the code inside debug-mode checks is not executed. As we saw above, this includes all assertions, which are not run. We can see this in the screenshot above.

In particular, note that when __debug__ is False, neither assert True nor assert False does anything. So, in particular, assert False did not raise AssertionError, while in the debug mode it would. This is strictly because __debug__ was False, meaning that assertions were switched off.

How to use debug to optimize code execution

As mentioned above, code executed in the production mode is optimized. It means only one thing: that any code in debug-mode checks will not be executed. Therefore, you can use __debug__ to add code to be executed only in the debug mode; in the production mode, this code will be ignored. That way, your production code will be faster – of course assuming it contains debug-mode checks, including assertions.

In order to achieve this, you can manually add code to a debug-mode check:

if __debug__:
    if x < 7:
        debug_logger.warning(f"x is below seven: {x = };"
                              " hence it's set to 7")
        x = 7
    elif x > 13:
        debug_logger.warning(f"x is over thirteen: {x = };"
                              " hence it's set to 13")
        x = 13
    else:
        debug_logger.info(f"x is fine: {x = }")

If you run the code in the production mode, the contents of this if block will not be reached, and debug_logger will not log anything. Imagine you have plenty of such checks (e.g., in a long loop); ignoring them can make the code much faster.

I can imagine this is something a little tricky to think about. My suggestion is, next time you write your code, think if there is anything you would like to be executed only in the debug mode but not in the production mode. Sometimes, you will not be able to figure out a single thing; other times, you may find something like that.

Always, however, you should be able to find locations in which an assertion would do a good job. We'll discuss it below.

To summarize, remember about two things:

  • When you use many assertions and debug checks, the code can be significantly slower.
  • If you want to run some code irrespective of the mode, why at all should you check if __debug__ is true or not? Of course, when using the assert statement, it's done under the hood – but we already know that it's done and how it's done. Nonetheless, remember not to add code to debug checks if you want to run it in both the debug and the production modes.

When to use assertions

At last! Now that we know what __debug__ and the debug mode are, we're ready to discuss assertions.

As we saw above, in terms of code, an assertion is a check that is done if the code is executed in the debug mode: when the condition is true, nothing happens, and when it's not, AssertionError is raised.

It does not fully explain when you should use assertions. Simply put, use assertions

  • in testing (which is always performed in the debug mode), to check whether or not a particular test passes;
  • in development mode, to check conditions that should never ever happen.

As for testing, all should be clear. As I wrote above pytest uses assertions as the main tool for checking conditions. You normally run unit tests in the debug mode. Try, however, to run pytest in the production mode, so using python -O -m pytest, and you will see the following warning:

PytestConfigWarning: assertions not in test modules or plugins
will be ignored because assert statements are not executed by 
the underlying Python interpreter (are you using python -O?)

    self._warn_about_missing_assertion(mode)

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html

As you see, when you want to run pytest in the production mode, it's in fact run in a what we could call a hybrid mode. All assert statements in test functions will be executed, but any assertions in the actual code will not be. This is what PytestConfigWarning tells us.

As for the development mode, the situation is different. As you read above, the assert statement helps you check whether or not a condition that should never ever happen is true or not. This may sound strange at first glance. Why should I check a condition that should never happen? Isn't this something like checking whether a cat is a dog?

Exactly! You got it! Using assertions in code is like checking whether a cat is a dog. Sure, we know it is not – and the assertion assert cat is not dog does not really aim to check whether cat is not dog, which we do know to be true, but whether the code is correct.

In other words, assertions help you check whether the code works the way it should. When it does not, it can lead to such impossible things as a cat being a dog, an integer being a string, a natural number being negative, sample size being greaten than population size, and the like. Thus, remember what an assertion is: you check something that is obvious – and when it's not, the assertion fails and you know that something is incorrect in the code or in the logic that the code implements.

The assertion assert cat is not dog does not really aim to check whether cat is not dog, which we do know to be true, but whether the code is correct.

If according to your code cat is dog, which is of course not true, the assertion will fail and raise AssertionError. This means that the code is incorrect.

So, now we know when to use assertions. First, you can use them in testing. Second, you can use them to make sure that the code is correct, by adding assertions with conditions that must be true. If such an assertion fails, the code is incorrect – because a cat cannot be a dog.

If such an assertion fails, the code is incorrect – because a cat cannot be a dog.

There's one important thing to add. Do not overdo with assertions. Do not put them wherever you can, only because you can. Use them in important places, where they mean something important. Use them to catch important flaws.

Do not overdo with assertions. Do not put them wherever you can, only because you can.

When not to use assertions

Now that we know when to use assertions, it should be clear when we should not use them.

First of all, you should not use assertions to handle regular exceptions. These can be incorrect argument values, data, a wrong password, things like that. Such errors should be handled in a regular way.

You should not use assertions to handle regular exceptions.

Let's see. Consider the following function:

def preprocess_text(text: str) -> str:
    assert isinstance(text, str)
    return text.lower().strip()

The function aims to preprocess a string in a particular way. In our case, preprocessing is very simple, text.lower().strip(), but that's just an example of what the function could do. The function also checks whether the provided value of argument text has a correct type, that is, str; and if not, it raises an exception. Right?

Wrong! In order to check the type, the function uses the assert statement, and we already know that it's not correct. First, note that if you provide an object of a different type, AssertionError is raised, and it does not say what it should – that the type is incorrect. Python has TypeError for this.

Second, note that in the production mode, this check would not be executed. Is this really the behavior you would want for this function? I'd rather say if you need to check the type of text, then you should do it in both modes. Here, you could get a different behavior in the debug mode and in the production mode. I suppose many developers using assert in such situations may not know of this.

We know what's wrong. The function should not use assert. Instead, it can use an if statement combined with the raise statement, or a dedicated tool, such as the easycheck package:

GitHub – nyggus/easycheck: A module offering Python functions for simple and readable assertions to…

I'm planning to write a longer article about easycheck, but you can already read about its particular use case here:

Comparing floating-point numbers with easycheck

In our above function, we can add an easycheck check as follows:

import easycheck

def preprocess_text(text: str) -> str:
    easycheck.check_type(
        text, 
        expected_type=str,
        handle_with=TypeError,
        message="Argument text must be string, "
                f"not {type(text).__name__}"
    )
    return text.lower().strip()

The above check can be read in the following – natural in my eyes – way: Check type of text; it should be str; if it is not str, raise TypeError, with the following message: f"Argument text must be string, not {type(text).__name__}".

So, when you provide, for instance, an integer, you will see the following:

>>> preprocess_text(108)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: Argument text must be string, not int

Let's summarize this example:

Question: Should I use assertion in preprocess_text() in order to check if text is a string?

Answer: No

Question: Why not?

Answer: You should not use assertion here because a different type of text is not an impossible situation that should never happen. It can happen when a user provides the text argument of an incorrect type – and this definitely can happen.

Question: What should I use instead, then?

Answer: You can use an if check of text‘s type, and raise TypeError when it's incorrect. Or you can use easycheck, a dedicated tool for such situations.

Production mode, tests and assertions

Now, an important question is whether at all you should use the production mode, invoked using the -O flag.

In his Clean Code in Python. Develop maintainable and efficient code book, Mariano Anaya says that you shouldn't. You shouldn't, because assertions help catch bugs, so why should you resign this opportunity only because you're running your code in production? When an assertion fails, something serious is wrong with the code – and the code will break anyway, but quite possibly later. Best to raise an exception as soon as possible.

I fully agree with the above approach, but…

Sometimes it's better to use the production mode. This is when the execution time is critical and the assertions used in the code make it significantly slower. Assuming that the code is well tested, you may prefer to switch off all assertions, only to make the app faster – especially when you used a lot of them.

In some projects, the decision whether or not to use the production mode is simple. When the execution time does not matter, run your production code in the debug mode. It's like testing the production code in production – there is no better testing. In other projects, the decision is simple, too – when execution time matters, and I mean it does matter, use the production mode. In that case, the code should be well covered with unit and integration tests, and all the tests should be run before deployment of each new version.¹ Nonetheless, if you want to use the production mode, I think you should run your tests in both the debug and the production modes. The latter – which I actually called above the hybrid mode – can help you catch bugs that would not be caught in the debug mode. Since I've never found a word about this topic, I am planning to write a dedicated article, with in-depth explanation and examples; I will link it here when it's ready and published.

There will be in-between projects, in which the decision will not be that simple. You will have to decide if you should run assertions or switch them off based on the project's assumptions, but also based on the quality of your code and tests as well as on the test coverage.

Conclusion

I wrote this article because I noticed that many Python developers do not understand what an assertion is. I sympathize with them. I was there. At some point of my Python journey, I didn't understand assertions, either.

I hope the article explains both assertions and __debug__ in sufficient detail. Let's summarize what we discussed:

  • Assertions are executed only in the debug mode, which is the default Python mode. Run Python in production mode by using the -O flag, that is, python -O.
  • Use assertions to check conditions that must be true. If they fail, something is wrong with the code.
  • Do not use assertions to check other things, like conditions related to argument values. Such checks should be done using if checks combined with raise, or dedicated tools such as the easycheck Python package.
  • Use __debug__ to add code to be executed in the debug mode but not in the production mode.
  • When you run your production code in the production mode, you should also run your unit and integration tests in the production mode.

All in all, always decide whether to use the production mode or not based on the particular situation.

If you'd like to learn a little more about AssertionError, the article below shows a little trick with it; namely, it shows how to overwrite it with a different type of exception. For instance, you may wish to use in unit tests a custom project exception instead of the built-in AssertionError. I don't think that's something you should actually use in your production code, but that's something that can help you understand Exception Handling and assertions – and Python, for that matter.

How to Overwrite AssertionError in Python and Use Custom Exceptions

If you're interesting in how to format long assertions, you may find the following article interesting:

Don't Surround Python Assertions with Parentheses

You will read there what to do when an assertion is too long to fit a single line – and why you should never surround its condition and message by parentheses.

Footnotes

¹ Well, you should always run tests before deploying a new version. Sometimes, however, you may decide to run only the tests of the module being re-deployed – but that depends on the architecture of the application. Anyway, it's always safer to run all tests before deployment. We write them to run them, don't we?


Thanks for reading. If you enjoyed this article, you may also enjoy other articles I wrote; you will see them here. And if you want to join Medium, please use my referral link below:

Join Medium with my referral link – Marcin Kozak

Tags: Data Science Error Error Handling Exception Handling Python

Comment