When to use assert

Chris Angelico rosuav at gmail.com
Fri Oct 24 10:18:24 CEST 2014


On Fri, Oct 24, 2014 at 6:49 PM, Steven D'Aprano
<steve+comp.lang.python at pearwood.info> wrote:
> addresses = [get_address(name) for name in database]
> assert all(address for address in addresses)
> # ... much later on ...
> for i, address in enumerate(addresses):
>     if some_condition():
>         addresses[i] = modify(address)
>         assert addresses[i]
>
>
> will either identify the culprit, or at least prove that neither
> get_address() nor modify() are to blame. Because you're using an assertion,
> it's easy to leave the asserts in place forever, and disable them by
> passing -O to the Python interpreter in production.

The first assertion is fine, assuming that the emptiness of your
address corresponds to falsiness as defined in Python. (This could be
safe to assume, if the address is an object that knows how to boolify
itself.) The second assertion then proves that modify() isn't
returning nothing, but that might be better done by sticking the
assertion into modify itself.

And that's what I'm talking about: checking a function's postcondition
with an assert implies putting that assertion after every call, and
anything that you have to do every time you call a function belongs
inside that function. Imagine writing this kind of defensive code, and
then having lots of places that call modify()... and missing the
assert in some of them. Can you trust what you're getting back?

> [Aside: it would be nice if Python did it the other way around, and require
> a --debugging switch to turn assertions on. Oh well.]

Maybe. But unless someone actually tests that their assertions are
being run, there's the risk that they're flying blind and assuming
that it's all happening. There'll be all these lovely "checked
comments"... or so people think. Nobody ever runs the app with
--debugging, so nobody ever sees anything.

> Assertions and test suites are complementary, not in opposition, like belt
> and braces. Assertions insure that the code branch will be tested if it is
> ever exercised, something test suites can't in general promise. Here's a
> toy example:
>
> def some_function(value):
>     import random
>     random.seed(value)
>     if random.random() == 0.25000375:
>         assert some_condition
>     else:
>         pass
>
>
> Try writing a unit test that guarantees to test the some_condition
> branch :-)
>
> [Actually, it's not that hard, if you're willing to monkey-patch the random
> module. But you may have reasons for wanting to avoid such drastic
> measures.]

Easy. You craft a test case that passes the right argument, and then
have the test case test itself. You will want to have a script that
figures out what seed value to use:

def find_seed(value):
    import random
    for seed in range(1000000): # Restrict to not-too-many tries
        random.seed(seed)
        if random.random() == value: return seed

I didn't say it'd be efficient to run, but hey, it's easy. I've no
idea how many bits of internal state the default Python RNGs use, but
testing a million seeds took a notable amount of time, so I told it to
fail after that many. (And I didn't find one that gave that result.)

But actually, it would be really simple to monkey-patch. And in any
non-toy situation, there's probably something more significant being
tested here... unless you really are probing a random number generator
or something, in which case you probably know more about its
internals.

> I like this style:
>
> assert x in (a, b, c)
> if x == a:
>     do_this()
> elif x == b:
>     do_that()
> else:
>     assert x == c
>     do_something_else()

If all your branches are simple function calls, I'd be happy with a
KeyError instead of an AssertionError or RuntimeError.

{a:do_this, b:do_that, c:do_something_else}[x]()

I was talking to a student this week who had a long if/elif chain that
translated keywords into values, something like this:

def get_whatever_value(kwd):
    if kwd == 'value_should_be_50':
        return 50
    elif kwd == 'value_wants_to_be_75':
        return 75
    elif kwd == 'one_hundred':
        return 100

There was no 'else' clause, so in the event of an incorrect keyword,
it would return None. Now, I could have advised adding an "else
ValueError" or an assertion, but my preferred technique here is a
simple dict lookup. Simpler AND guarantees that all inputs are
checked.

>>> Or is that insufficiently paranoid?
>>
>> With good tests, you're probably fine.
>
> Is it possible to be too paranoid when it comes to tests?

Yeah, it is. I said earlier about checking that len() returns an
integer. The only way[1] for len(some_object) to return a non-integer
is for someone to have shadowed len, and if you're asserting to see if
someone's maliciously shadowing builtins, you *really* need a hobby.
But hey. Maybe asserting *is* your hobby!

ChrisA

[1] Cue the response pointing out some way that it'll return something
else. I wasn't able to break it, though.



More information about the Python-list mailing list