Assertions and Exceptions

I recently had a discussion about the difference of Python’s assert keyword vs. raising “normal” exceptions. For quite a long time, I was uncertain when to use assert, too. In this article, I’ll present some examples and rules of thumb for when using assert or raising exceptions is – in my opinion – more appropriate.

When do I use assert?

You use assert to check and document invariants of your code. Invariants are conditions in the middle of your code that should always be true (For example: “At this point in time x should always be a number between 0 and 10”).

Assertions can help others (including future-you) to understand your code and they should make your program crash if the assertion becomes false (because of a programming error).

Here are some examples for when assert is useful:

# Bad:

def func(a, b):
    x = [a, b]
    ...  # Some operations on "x"
    # x should still contain 2 elements:
    return *x


# Good:

def func(a, b):
    x = [a, b]
    ...  # Some operations on "x"
    assert len(x) == 2, f'len(x) == {len(x)}'
    return *x
# We have widget like this:
#
#   Step size: ( ) auto   (•) manual: [___300] seconds

# Bad:

if self.button_auto.isChecked():
    return None
else:
    # The user provided a manual step size:
    return self.step_size_input.value()


# Good:

if self.button_auto.isChecked():
    return None
else:
    assert self.button_manual.isChecked()
    return self.step_size_input.value()
# Bad:

def fill_list(self):
    # self.dest should be empty, but self.source not:
    while self.source_not_empty():
        self.dest.append(self.take_from_source())


# Good:

def fill_list(self):
    assert len(self.dest) == 0
    assert len(self.source) > 0
    while self.source_not_empty():
        self.dest.append(self.take_from_source())
    assert len(self.dest) > 0

Validation of input parameters

Sometimes, assert is used to validate input parameters for functions. You should avoid this because assert statements are not executed when Python is invoked with -O[O]. In this case, you would suddenly loose all input validation.

AssertionErrors also carry less semantics than “normal” exceptions – like TypeError when the user passed a string and not a number, or ValueError when the user passed a negative number when only positive numbers are allowed.

# Bad:

def sum(a, b):
    assert isinstance(a, int), 'a must be an int'
    assert a >= 0, 'a must be >= 0'
    ...

# Good:

def sum(a, b):
    if not isinstance(a, int):
        raise TypeError(f'a must be an int but is a {type(a)}')
    if a < 0:
        raise ValueError(f'a must be >= 0 but is {a}')
    ...

There are, however, exceptions possible for this rule. Imagine the following snippet where we want to add/delete a user from a group on an LDAP server:

def add_to_group(conn, user, group):
    _modify_group(conn, user, group, ldap3.MODIFY_ADD)

def delete_from_group(conn, user, group)
    _modify_group(conn, user, group, ldap3.MODIFY_DELETE)

def _modify_group(conn, user, group, op):
    assert op in {ldap3.MODIFY_ADD, ldap3.MODIFY_DELETE}
    conn.modify(group.dn, {'uniqueMember': [(op, [user.username])]})

In this case, it’s appropriate to use assert because _modify_group() should never be called by our users but only by add_to_group() and delete_from_group().

What kind of exceptions should I raise?

Within you own code, the built-in exception types – especially ValueError, AttributeError, TypeError and RuntimeError – are usually sufficient.

Libraries often define their own exception hierarchy with their own base class. For example, Requests has RequestError from which all other exception types inherit.

This makes it a lot easier for users to catch your library’s exceptions. If you raise all kinds of exceptions (and then you don’t document that, too), your users will end up using a broad/bare excepts which is bad.

except [[Base]Exception] is bad because it results in all kinds of nasty side effects. For example, it would also catch (and possibly ignore) KeyboardInterrupt (Ctrl+C) or AsstionError.

And that’s why you should also never raise [Base]Exception.

TLDR

  • Use assert to detect programming errors and conditions that should never occur and which should crash your program immediately, e.g., invariants of an algorithm.

    Warning: Assertion checks are stripped and not executed if Python is invoked with the -O or --OO option!

  • Raise an exception for errors that are caused by invalid user input or other problems with the environment (e.g., network errors or unreadable files).

  • Never raise or catch [Base]Exception.