GCP 5 - Using assertions

Attention

Finnish university students are encouraged to use the CSC Notebooks platform.
CSC badge

Others can follow the lesson and fill in their student notebooks using Binder.
Binder badge

The goal of defensive programming is to try to maximize the reliability and overall quality of a piece of software. For us, this means that we should take steps to handle unexpected input values in our code, and to provide helpful error messages that provide meaningful guidance to the user when a program raises an exception. We can take steps toward writing more reliable software by utilizing a helpful features in Python: Assertions.

Assertions

Assertions are a way to assert, or ensure, that the values being used in your scripts are going to be suitable for what the code does. Let’s start by considering the function convert_kph_ms that converts wind speeds from kilometers per hour to meters per second. We can define and use our function in the cell below.

def convert_kph_ms(speed):
    """Converts velocity (speed) in km/hr to m/s"""
    return speed * 1000 / 3600


wind_speed_km = 9
wind_speed_ms = convert_kph_ms(wind_speed_km)

print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")
A wind speed of 9 km/hr is 2.5 m/s.

This all seems fine, but you might want to ensure that the values for the wind speed are not negative numbers, since speed is simply the magnitude of the wind velocity, which should always be positive or zero. We can enforce this condition by adding an assertion to our function.

def convert_kph_ms(speed):
    """Converts velocity (speed) in km/hr to m/s"""
    assert speed >= 0.0
    return speed * 1000 / 3600


wind_speed_km = 9
wind_speed_ms = convert_kph_ms(wind_speed_km)

print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")
A wind speed of 9 km/hr is 2.5 m/s.

OK, so everything still works when using a positive value, but what happens if we now give a negative value for the wind speed? Let’s check!

wind_speed_km = -27
wind_speed_ms = convert_kph_ms(wind_speed_km)

print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_475/3501119874.py in <module>
      1 wind_speed_km = -27
----> 2 wind_speed_ms = convert_kph_ms(wind_speed_km)
      3 
      4 print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")

/tmp/ipykernel_475/757674053.py in convert_kph_ms(speed)
      1 def convert_kph_ms(speed):
      2     """Converts velocity (speed) in km/hr to m/s"""
----> 3     assert speed >= 0.0
      4     return speed * 1000 / 3600
      5 

AssertionError: 

OK, so now we get an AssertionError when a negative value is provided. This AssertionError is produced because of the assert statement we entered in the function definition. If the condition listed after assert is false, an AssertionError is raised.

This is a definite improvement, however, it would be much better to provide the user with some information about why this assertion exists. Fortunately, we can do this simply by adding some text after the assertion condition.

def convert_kph_ms(speed):
    """Converts velocity (speed) in km/hr to m/s"""
    assert speed >= 0.0, "Wind speed values must be positive or zero"
    return speed * 1000 / 3600


wind_speed_km = -27
wind_speed_ms = convert_kph_ms(wind_speed_km)

print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_475/3363017470.py in <module>
      6 
      7 wind_speed_km = -27
----> 8 wind_speed_ms = convert_kph_ms(wind_speed_km)
      9 
     10 print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")

/tmp/ipykernel_475/3363017470.py in convert_kph_ms(speed)
      1 def convert_kph_ms(speed):
      2     """Converts velocity (speed) in km/hr to m/s"""
----> 3     assert speed >= 0.0, "Wind speed values must be positive or zero"
      4     return speed * 1000 / 3600
      5 

AssertionError: Wind speed values must be positive or zero

Nice! Now we see that when the AssertionError is raised, the message informs us about why it happened without having to interpret the code. The message also makes it easy to fix our value for wind_speed_km to work with the convert_kph_ms function.

More generally, assertions take on the following form:

assert <some test>, 'Error message to display'

So we start with the assert statement, then give a logical test for some condition. If the test is true, nothing happens and the code continues. If not, the code stops and an AssertionError is displayed with the text written after the comma on the line containing the assert statement.

Multiple assertions

A bad example

Of course, you may want to have several assertions in a function in order to ensure it works as expected and provides meaningful output. In our case, we might first want to check that the value provided to be converted is a number. If not, we would not be able to convert the units. Let’s add a second assertion to make sure our function is “safe”.

def convert_kph_ms(speed):
    """Converts velocity (speed) in km/hr to m/s"""
    assert (
        type(speed) == int or type(speed) == float
    ), "Wind speed values must be numbers"
    assert speed >= 0.0, "Wind speed values must be positive or zero"
    return speed * 1000 / 3600


wind_speed_km = "dog"
wind_speed_ms = convert_kph_ms(wind_speed_km)

print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_475/244533844.py in <module>
      9 
     10 wind_speed_km = "dog"
---> 11 wind_speed_ms = convert_kph_ms(wind_speed_km)
     12 
     13 print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")

/tmp/ipykernel_475/244533844.py in convert_kph_ms(speed)
      3     assert (
      4         type(speed) == int or type(speed) == float
----> 5     ), "Wind speed values must be numbers"
      6     assert speed >= 0.0, "Wind speed values must be positive or zero"
      7     return speed * 1000 / 3600

AssertionError: Wind speed values must be numbers

OK, so that works. Now, if the user attempts to give a data type that is not int or float, the function will raise an AssertionError indicating a number is expected for the function to work. This is fine, but as noted below, there are reasons why you may not want to include assertions of this type in a function.

Warning

You might think that it would be useful to use an assertion to check the type of speed in our function in order to make sure that you don’t get a TypeError as occurred in the previous section. It turns out that this is not really a good idea. The reason is that the philosophical idea of a TypeError is to indicate you have incompatible data types. With that in mind, why would you raise an AssertionError to do the same thing?

A better example

So we don’t want to check our data type compatibility using assertions, but we can include a second assertion to ensure the maximum of the input wind speed is a reasonable number. In this case, we can asssume that the wind speed being converted was measured on Earth, and thus should be lower than the fastest wind speed ever measured, 408 km/hr. Let’s add that condition.

def convert_kph_ms(speed):
    """Converts velocity (speed) in km/hr to m/s"""
    assert speed >= 0.0, "Wind speed values must be positive or zero"
    assert speed <= 408.0, "Wind speed exceeds fastest winds ever measured"
    return speed * 1000 / 3600


wind_speed_km = "409"
wind_speed_ms = convert_kph_ms(wind_speed_km)

print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_475/3620763190.py in <module>
      7 
      8 wind_speed_km = "409"
----> 9 wind_speed_ms = convert_kph_ms(wind_speed_km)
     10 
     11 print(f"A wind speed of {wind_speed_km} km/hr is {wind_speed_ms} m/s.")

/tmp/ipykernel_475/3620763190.py in convert_kph_ms(speed)
      1 def convert_kph_ms(speed):
      2     """Converts velocity (speed) in km/hr to m/s"""
----> 3     assert speed >= 0.0, "Wind speed values must be positive or zero"
      4     assert speed <= 408.0, "Wind speed exceeds fastest winds ever measured"
      5     return speed * 1000 / 3600

TypeError: '>=' not supported between instances of 'str' and 'float'

This is a better example for two reasons:

  1. We now allow a TypeError when incompatible data types are used in our function, which is a clear and familiar error message.

  2. We use assertions to check the values used in the function make sense for its intended use. If we want to help users convert wind speeds on Earth, we provide bounds that make sure they are using reasonable input values. Thus, we help them use our function the correct way.

Combined, these assertions ensure our function handles common mistakes and provide the user with helpful feedback to be able to use the function properly.

Note

One thing that is important to note about assertions is that although we use them here to check that our function input values are reasonable, this is not generally the suggested use. Instead, more advanced programmers recommend using assertions only to test that your code is working properly internally. For example, you would use assertions to check for things that should not happen, such as functions that duplicate values in lists when they should not. The reason it is not recommended to use assertions for testing user input values or the existence of files is that assertions can be disabled using flags when running a Python program. Thus, it is possible they could be ignored entirely. This is fine when debugging code, but obviously not desired when users are running your programs. If you’re interested in more details, feel free to check out the [article on using assertions in the Python wiki}(https://wiki.python.org/moin/UsingAssertionsEffectively).

More information about assertions

More information about assertions can be found on the Software Carpentry website.