Assertions and Exceptions
Posted on
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 - assertto 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 - -Oor- --OOoption!
- 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.