6

Sometimes when I look at other people's code I see functions that make a bunch of assumptions about the inputs but do not explicitly assert their assumptions. For example, look at the code below:

def func(a: list, b: list, c: int):
    total = 0
    for i in range(len(a)):
        total = a[i] + b[i]
    return total/c

My first instinct when I see code like this is to add a bunch of assert statements, like so:

def func(a: list, b: list, c: int):
    assert len(a) <= len(b)
    assert c, "cannot be 0"
    total = 0
    for i in range(len(a)):
        total = a[i] + b[i]
    return total/c

My argument is that I would much rather get an AssertionError so I know the exact problem (especially if there's a useful message) than an IndexError or something else and then have to figure out what the root cause is. Sometimes I see 5 or 6 assumptions made about the input, but in practice I don't see functions starting with lots of assert statements very often. I'm tempted to add a bunch of asserts to some code I found to make debugging easier. Is there any reason not to do this?

EDIT: Another way of asking this is if I get an error while running code and debug it to realize that input x from two calls higher in the traceback should always have some property (e.g. always be positive), is there any reason not to just add an assert statement right away in the code?

EDIT2: Here's an example from a popular code repo. In this case, the argument direction has to be in range(8). If it is not, the user gets an error that says

Exception has occurred: UnboundLocalError
local variable 'targ_pts' referenced before assignment

To me, this is much harder to debug than if it started with an assert statement like assert direction in range(8), "skew direction must be integer between 0 and 7". Should an assert statement be added in this case?

5
  • 5
    Assertions are for things that should never happen, not just invalid input. By all means validate input, but assertions aren't the way to do it (if len(a) != len(b): raise ValueError("a and b should be same length") and if c == 0: raise ValueError("c cannot be 0"), in this case. Cross-site dupe: stackoverflow.com/questions/944592/….
    – jonrsharpe
    May 26 '20 at 6:54
  • 2
    IMHO, an invalid input is something that should never happen. In Design by contract (en.wikipedia.org/wiki/Design_by_contract) preconditions check against invalid inputs, and are specified as a set of assertions.
    – mgoeminne
    May 26 '20 at 9:49
  • 2
    Do you consider -1 an invalid input for math.sqrt()? Note that a calculator shouldn't crash with an AssertionError when the user tries to compute the square root of a negative number; instead it should show an error to the user. May 26 '20 at 9:52
  • @mgoeminne : That's not a matter of opinion. May 31 '20 at 19:46
  • 1
    @Sapphire_Brick OK, I'll rephrase. From my practical experience, and according to all the reference books I have ever read… Does it sound better?
    – mgoeminne
    Jun 3 '20 at 12:03
8

In general, it is a good idea to make your code easier to debug by adding extra tests, assertions and validations. Using a lot of asserts for this is not even a very new idea, it is a common practice for decades especially in languages like C, where without explicit checks program can easily run into undefined behaviour.

But let me focus on your literal question:

Is there any reason not to do this?

Yes, there are some reasons why it is sometimes not a good idea to clutter each and every function with tests for every possibly violated precondition. Asserts can have a huge benefit, but they also come for a cost:

  • one has to write, debug and and maintain extra code

  • this extra code can sometimes decrease readability, especially when there are lot of asserts not only at the beginning of a function, but througout the whole function body

  • if the code only checks things which would fail automatically at the next line after the assert statement, and the assert error message is not really telling more than the failure message of the run time system one would get either, without an assert, then the check becomes superfluous.

  • in some cases, asserts can have a negative performance impact

Looking at the example in your question, I guess you intentionally picked a case where the cost/benefit relationship looks not too bad for using asserts - both asserts give an early failure, before the loop is executed, and they make the preconditions more explicit, so they are also some kind of sensible documentation, increasing the readibility, not decreasing it.

However, note in both cases the benefit is not really that huge - the run time system would throw an exception in both cases either, just a little bit later, but still within the scope of this function.

So at the end, it is a trade-off. I would recommend to focus on using asserts

  • for early checks against non-obvious issues, maybe issues which would not been raised automatically by the run time system

  • if the technical error messages raised by the run-time are not clear enough, or when they will pop-up to the user and you want to replace them by a less cryptical text

  • when they make the code more readable, not less

  • when the performance impact is negligible.

5

(Speaking not just in the context of Python ...)

I fill(!) my source code with assertions and self-checks ... and I never(!) "compile them out" when generating "production" code.

And here's why:

"The very hardest thing about any programming error is ... first, knowing that it exists, and then, having unpleasantly having just had this reality thrown into your face(!!), knowing exactly what and where it is!"

Which party is in the very-best situation to tell you this? Yes! The software itself.

Therefore: let every single part of your ever-growing software system distrust every other part. So that, if the whole thing "successfully runs to completion," you know by this accomplishment that this-or-that possible error condition could not have occurred (unless your tests were wrong).

"No one else is in the right position to do this for you. The computer itself has to do it."

1
  • Talk about "failing fast and hard." May 26 '20 at 14:29
4

Should you validate input? Yes, by all means!

Should you do that by calling assert? That's not so clear cut...

Bear in mind that some language runtimes, e.g, C or C++, explicitly call abort as a consequence of failed asserts. Whether that's desirable behavior strongly depends on the application, the subroutine in question, and whether the program state is recoverable -- i.e., in your example, can you prompt the user for some divisor other than zero?

0

The assertions are fine, and do help illustrate how the code is supposed to work. But one reason they are needed to explain how the code works is that the code is full of "WTFs?". Better naming and and some rethinking would help.

  1. Why is the length of only the first list important? Why not take the shorter of either list as your length?
    • rename it to "controlList", "majorList", etc...?
  2. The second list is "less important",
    • rename it to "subsidiaryList", "minorList", ...?
  3. Why is division by c part of a function?
    • Why not just return the (partial) sum of the two lists and let them divide later?
    • or rename c to something meaningful that explains why the division is here
  4. For some important reason, it it important to only sum over part of the lists. Otherwise you could just write something like (sum(a) + sum(b)) / n What is this important reason?
    • give this func a meaningful name!
    • and maybe some comments

Sure, you only provided "example code", that's probably not your real code. But, the point is, if you are using asserts primarily to explain how the code works, you are misusing them.

0

There's nothing wrong about it, it's just duck typing for parameters.

Sometimes when I look at other people's code I see functions that make a bunch of assumptions about the inputs but do not explicitly assert their assumptions.

Their code is poorly written. Defensive programming allows programs to be more maintainable and easier to track down bugs.
As the Zen of Python puts it:

Errors should never pass silently.
Unless explicitly silenced.

Not the answer you're looking for? Browse other questions tagged or ask your own question.