Assertions are statements to ensure that a particular condition or state holds true at a certain point in the execution of a program. These checks are usually only performed in debug builds, which means that you must ensure that the expressions in the assertions are side-effect free.

Because assertions are only validated in debug builds, you can abuse them to make your code more readable without impacting performance and without having to write a comment. Every time you write a line in which you assume a particular machine state that is not clearly implied by the code immediately preceding the new line, write an assertion.

Let’s look at a real world example derived from Kyua’s code. Consider the following extremely-simplified function which implements the help command:

def help_command(args):
    if len(args) == 0:
        show_general_help()
    else:
        show_command_help(args[0])

With this code alone, try to answer these questions: “What happens if args, which apparently is a subset of the arguments to the program, has more than 1 item? Are the additional arguments ignored and thus we have a bug in the code, or has the args vector been pre-sanitized by the caller to not have extra arguments?” Well, you can’t answer this question because there is nothing in the code to tell you what the case is.

If I now show you the caller to the function, you can get an idea of what the expectation is:

def main(args):
    commands = {}
    commands['help'] = cli.Command(min_args=0, max_args=1,
                                   hook=help_command)
    ...
    cli.dispatch(commands, args)

Aha! There happens to be an auxiliary library that processes the command line and dispatches calls to the various subcommands based on a declarative interface. This declarative interface specifies what the maximum number of arguments to the command can be, so our function above for help_command was correct: it was handling all possible lengths of the input args vector.

But that’s just too much work to figure out a relative simple piece of code. A piece of code needs to be self-explanatory with as little external context as possible. We can do this with assertions.

The first thing you can do is state the precondition to the function as an assertion:

def help_command(args):
    assert len(args) <= 1
    if len(args) == 0:
        show_general_help()
    else:
        show_command_help(args[0])

This does the trick: now, without any external context, you can tell that the args vector is supposed to be empty or have a single element, and the code below clearly handles both cases.

However, I argue that this is still suboptimal. What is the complementary condition of len(args) == 0? Easy: len(args) > 0. Then, if that’s the case, how can the else path be looking at the first argument only and not the rest? Didn’t someone overlook the rest of the arguments, possibly implying that the input data is not fully validated? This would be a legitimate question if the function was much longer than it is and reading it all was hard. Therefore, we would do this instead:

def help_command(args):
    if len(args) == 0:
        show_general_help()
    else:
        assert len(args) == 1
        show_command_help(args[0])

Or this:

def help_command(args):
    if len(args) == 0:
        show_general_help()
    elif len(args) == 1:
        show_command_help(args[0])
    else:
        assert False, 'args not properly sanitized by caller'

Both of these alternatives clearly enumerate all branches of a conditional, which makes the function easier to reason about. We will get to this in a future post.

Before concluding, let’s outline some cases in which you should really be writing assertions:

  • Preconditions and postconditions.
  • Assumptions about state that has been validated elsewhere in the code, possibly far away from the current code.
  • Complementary conditions in conditionals where not all possible values of a type are being inspected.
  • Unreachable code paths.