# Understanding assert For Debugging In Python

Python's `assert` statements are one of several options for debugging code in Python.

Python's `assert` is mainly used for debugging by allowing us to write [sanity tests](https://en.wikipedia.org/wiki/Sanity_check) in our code. These tests are performed to ensure that a particular condition is **True** or **False**. If the condition is **False**, an `AssertionError` is raised, indicating that the test condition failed.

## Understanding assert

Python's `assert` keyword is used to write `assert` statements that contain a condition or assumption that is tested against the condition from the program that we expect to be true.

If the condition matches the expected condition, nothing is displayed on the console and the execution continues, otherwise, an `AssertionError` is displayed. This exception interrupts program execution and indicates that the condition test failed.

### Syntax

The syntax of the `assert` statement is written in the following form:

`assert [condition], [error message]`

`condition` - the condition or assumption to be tested

`error message` - the error message we want to display in the console when the condition is failed.

### The assert In Action

Let's create some `assert` statements to perform code checks. Consider the following example, in which we are testing our program to see if it produces the expected results.

```python
def evaluate_num(num):
    if num > 5:
        return num * num
    else:
        return num * 2

val = evaluate_num(5)

"""Assert statement to check that upper code 
returns 10 on evaluating evaluate_num(5)"""

assert val == 10, "Condition failed." # We'll get nothing
```

The above code defines a function called `evaluate_num` that takes a parameter `num`. The function checks if the value of `num` is greater than 5. If it is, the function returns the square of `num` (`num * num`). Otherwise, if `num` is less than or equal to 5, the function returns `num` multiplied by 2 (`num * 2`).

The `assert` statement checks whether the variable `val` is equal to 10 after evaluating `evaluate_num(5)`. In this case, `evaluate_num(5)` returns 10, which means that the `assert` statement is true and we'll get nothing in the console.

Let's see what happens when we pass a `num` greater than 5.

```python
val = evaluate_num(6)

"""Assert statement to check that upper code 
returns 12 on evaluating evaluate_num(6)"""

assert val == 12, "Condition failed."
```

We called the `evaluate_num` with the argument `6`. Since `6` is greater than 5, the function will square the number `6` (`6 * 6`) which makes the variable `val` equal to 36, which makes our `assert` statement false. As a result, we'll get an `AssertionError` with the message `"Condition failed."`.

```python
Traceback (most recent call last):
  ....
    assert evaluate == 12, "Condition failed."
AssertionError: Condition failed.
```

## Controlling the Behavior of assert

We were able to write assertions in a single line by using the `assert` keyword, and this single-line `assert` statements are equivalent to the following expression:

```python
if __debug__:
    if not evaluate == 12:
        raise AssertionError("Condition failed.")
```

The above `if __debug__` conditional would function similarly to the `assert` statement written in the preceding code.

As a result of the above expression, the syntax of the `if __debug__` conditional would be:

```python
if __debug__:
    if not condition:
        raise AssertionError(error message)

# ----------------- OR ----------------- #

# For simple form assert statement without error message
if __debug__:
    if not condition:
        raise AssertionError
```

### \_\_debug\_\_

What exactly is `__debug__`, and how does it affect the behavior of `assert` statements in a Python program?

The `__debug__` is a built-in constant in Python that is set to `True` by default. However, we can change this to `False` by running Python in optimized mode with the `-O` command line option or by modifying the `PYTHONOPTIMIZE` variable.

```python
print(__debug__)

----------
True
```

As we can see when we printed the `__debug__`, we got `True` which indicates that our Python is not running in optimized mode.

Let's understand better with examples.

```python
# test.py
class Shopping:
    def __init__(self, product, price):
        self.product = product
        self.price = price

    def list(self):
        assert self.price > 0, "Price should not be 0 or negative."
        data = f"{self.product} is worth ${self.price}."
        return data

item_1 = Shopping("Perfume", 250)
print(item_1.list())

item_2 = Shopping("Band Aid", 0.75)
print(item_2.list())

item_3 = Shopping("Denim", -35)
print(item_3.list())
```

The above code defines the `Shopping` class, which has a `__init__` method that takes two parameters, `product` and `price`. These parameters' values are assigned to the instance variables `self.product` and `self.price`.

This class has another method called `list` that returns product information along with a price. This method includes an `assert` statement that determines whether the product's price is greater than `0`.

Then we created three instances of the `Shopping` class (`item_1`, `item_2`, and `item_3`) and passed in the various products and prices. When we run the above code, we get the following result.

```bash
Perfume is worth $250.
Band Aid is worth $0.75.
Traceback (most recent call last):
  ....
    assert self.price > 0, "Price should not be 0 or negative."
AssertionError: Price should not be 0 or negative.
```

The first two instances passed the test because the `assert` statement condition (`self.price > 0`) was met. As a result, we received the string, whereas in the third case, the price was set to `-35`, which did not satisfy the `assert` statement condition, and we received the `AssertionError` with the error message.

The following `if __debug__` conditional is equivalent to the `assert` statement we created in the method `list` within the class `Shopping`. If we had used the following code instead of the `assert` statement in the above code, the code would have worked perfectly.

```python
if __debug__:
    if not self.price > 0:
        raise AssertionError("Price should not be 0 or negative.")

# Equivalent to
assert self.price > 0, "Price should not be 0 or negative."
```

### Disabling Assertions

We can disable the assertion and prevent the `AssertionError` message from being displayed on the console. We'll try it manually first, then look at other safe options.

We could disable the assertion manually if we set `__debug__` to `False`. Let's see if we can complete this task within our program.

```python
# test.py
class Shopping:
    def __init__(self, product, price):
        self.product = product
        self.price = price

    def list(self):
        if __debug__ == False:
            if not self.price > 0:
                raise AssertionError("Price should not be 0 or negative.")
        data = f"{self.product} is worth ${self.price}."
        return data


item_1 = Shopping("Perfume", -1)
print(item_1.list())

item_2 = Shopping("Band Aid", 0.75)
print(item_2.list())

item_3 = Shopping("Denim", -35)
print(item_3.list())
```

Within our method `list`, we added a code snippet that checks the value of `__debug__`, if `__debug__` is set to `False`, it means that Python is running in **optimized mode** and the assertions are disabled.

Since assertions are disabled, the above code will produce no errors on the console.

```bash
Perfume is worth $-1.
Band Aid is worth $0.75.
Denim is worth $-35.
```

Note: This is not a good practice and is not recommended method to disable assertions.

### The -O Option

The `-O` flag is a command-line option that disables all assertions. Internally, this option sets the `__debug__` constant to `False`.

```python-repl
D:\SACHIN\Pycharm\assert_in_python>python -O
>>> print(__debug__)
False
```

Open the terminal and change the directory containing the Python file and run the following command:

```bash
D:\SACHIN\Pycharm\assert_in_python>python -O test.py
Perfume is worth $-1.
Band Aid is worth $0.75.
Denim is worth $-35.
```

The `python -O test.py` command enables the optimized mode and executes the Python file `test.py`. The `-O` flag instructs the Python interpreter to optimize the code by turning off assertions.

We would have gotten the `AssertionError` if we hadn't used the `-O` flag.

```bash
D:\SACHIN\Pycharm\assert_in_python>python test.py
Traceback (most recent call last):
  ....     
    assert self.price > 0, "Price should not be 0 or negative."
AssertionError: Price should not be 0 or negative.
```

### PYTHONOPTIMIZE Env Variable

By setting the `PYTHONOPTIMIZE` environment variable to `1`, we can run Python in optimized mode.

To set `PYTHONOPTIMIZE=1`, enter the following command in the terminal. This command will automatically run Python in optimized mode.

```bash
D:\SACHIN\Pycharm\assert_in_python>set PYTHONOPTIMIZE=1

D:\SACHIN\Pycharm\assert_in_python>python test.py
Perfume is worth $-1.
Band Aid is worth $0.75.
Denim is worth $-35.
```

When we check the status of the `__debug__` constant in the Python shell, it is automatically set to `False`.

```python-repl
D:\SACHIN\Pycharm\assert_in_python>python
>>> print(__debug__)
False
```

To undo the optimized mode, use the command `set PYTHONOPTIMIZE=0`.

## Performing Debugging

In this section, we'll write a bunch of `assert` statements and then test them with [pytest](https://docs.pytest.org/en/7.3.x/), a third-party package. This package contains a simpler syntax for writing tests.

Since this is an external package, we must install it by running the command `pip install pytest` in the terminal.

Make a Python file called `test_file.py` and place the following code, which includes tests, inside it.

```python
# test_file.py
import math
from os.path import isdir

# test_1
def test_sq():
    assert 5 * 5 == 20
    
# test_2
def test_search():
    assert "Py" in "GeekPython"
    
# test_3
def test_dir():
    assert isdir("test_file.py")
    
# test_4
def test_type():
    assert type([1, 2, 3]) == list
    
class TestCondition:
    # test_5
    def test_reverse(self):
        sequence = "GeekPython"
        assert sequence[:: -1] == "nohtyPkeeG"
    
    # test_6
    def test_value(self):
        assert round(math.pi) == 3.14
```

Now, open a terminal, navigate to the directory containing the Python file `test_file.py`, and type `pytest test_file.py`.

```python
D:\SACHIN\Pycharm\assert_in_python\test>pytest test_file.py
==================== test session starts ====================
platform win32 -- Python 3.10.5, pytest-7.3.2, pluggy-1.0.0
rootdir: D:\SACHIN\Pycharm\assert_in_python\test
plugins: anyio-3.6.2
collected 6 items

test_file.py F.F..F                                     [100%]

==================== FAILURES ====================
____________________ test_sq ____________________ 

    def test_sq():
>       assert 5 * 5 == 20
E       assert (5 * 5) == 20

test_file.py:6: AssertionError
____________________ test_dir ____________________ 

    def test_dir():
>       assert isdir("test_file.py")
E       AssertionError: assert False
E        +  where False = isdir('test_file.py')

test_file.py:12: AssertionError
____________________ TestCondition.test_value ____________________ 

self = <test_file.TestCondition object at 0x00000207D485FA60>

    def test_value(self):
>       assert round(math.pi) == 3.14
E       assert 3 == 3.14
E        +  where 3 = round(3.141592653589793)
E        +    where 3.141592653589793 = math.pi

test_file.py:23: AssertionError
==================== short test summary info ==================== 
FAILED test_file.py::test_sq - assert (5 * 5) == 20
FAILED test_file.py::test_dir - AssertionError: assert False
FAILED test_file.py::TestCondition::test_value - assert 3 == 3.14
==================== 3 failed, 3 passed in 0.32s ====================
```

The output of our tests produced by `pytest` is shown above, and we can see that three of them failed and three passed. The output provided full details for the three failed tests.

Note: `pytest` collects tests based on a naming convention. By default, classes containing tests must begin with `Test`, and any function in a file that should be treated as a test must also begin with `test_`. `pytest` will run all files of the form `test_*.py` or `*_test.py` in the current directory and its subdirectories. More details on the [naming convention](https://docs.pytest.org/en/7.3.x/explanation/goodpractices.html#test-discovery).

## Conclusion

`assert` is a built-in keyword in Python that is used to create `assert` statements that perform sanity checks in our code. It is used for testing and debugging.

The `assert` statement includes a condition that is used to determine whether the condition is **True** or **False**. If the condition is **False**, an `AssertionError` is thrown, indicating that the condition was not met.

Let's recall what we've learned:

* What is an `assert` statement with an example?
    
* `__debug__` constant in Python.
    
* Controlling the behavior of `assert` statements.
    
* Disabling assertions using the `-O` option and `PYTHONOPTIMIZE` environment variable.
    
* Debugging code using the `pytest` package.
    

---

🏆**Other articles you might be interested in if you liked this one**

✅[Python generators and the yield keyword - how they work](https://geekpython.in/python-generators-with-yield-statement)?

✅[Performing high-level path operations using pathlib in Python](https://geekpython.in/python-pathlib-module).

✅[Understanding the different uses of asterisk(\*) in Python](https://geekpython.in/asterisk-in-python).

✅[What is the difference between seek() and tell() in Python](https://geekpython.in/seek-and-tell-in-python)?

✅[Generate temporary files and directories using tempfile module in Python](https://geekpython.in/tempfile-in-python).

✅[How to change the string representation of the objects in Python](https://geekpython.in/str-and-repr-in-python)?

✅[\_\_init\_\_ and \_\_new\_\_: The concept of initializer and constructor in Python](https://geekpython.in/init-vs-new).

✅[What are context managers in Python](https://geekpython.in/context-managers-and-python-with-statement)?

---

**That's all for now**

**Keep Coding✌✌**
