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
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
.