assertions to validate function parameters

Steven D'Aprano steve at REMOVE.THIS.cybersource.com.au
Thu Jan 25 23:26:09 EST 2007


On Thu, 25 Jan 2007 16:54:05 +0000, Matthew Wilson wrote:

> Lately, I've been writing functions like this:
> 
> def f(a, b):
> 
>     assert a in [1, 2, 3]
>     assert b in [4, 5, 6]
> 
> The point is that I'm checking the type and the values of the
> parameters.

If somebody passes in a == MyNumericClass(2), which would have worked
perfectly fine except for the assert, your code needlessly raises an
exception.

Actually that's a bad example, because surely MyNumericClass(2) would
test equal to int(2) in order to be meaningful. So, arguably, testing that
values fall within an appropriate range is not necessarily a bad idea, but
type-testing is generally bad.

Note also that for real code, a bare assert like that is uselessly
uninformative:

>>> x = 1
>>> assert x == 3
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AssertionError


This is better:

>>> assert x == 3, "x must be equal to three but is %s instead" % x
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AssertionError: x must be equal to three but is 1 instead


This is even better still:

>>> if x != 3:
...     raise ValueError("x must be equal to three but is %s instead" % x)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ValueError: x must be equal to three but is 1 instead


And even better still is to move that test out of your code and put it
into unit tests (if possible).


 
> I'm curious how this does or doesn't fit into python's duck-typing
> philosophy.

Doesn't fit, although range testing is probably okay.


> I find that when I detect invalid parameters overtly, I spend less time
> debugging.

Yes, probably. But you end up with less useful code:

def double(x):
    """Return x doubled."""
    assert x == 2.0 and type(x) == float
    return 2*x

Now I only need to test one case, x == 2.0. See how much testing I don't
have to do? *wink*

There's a serious point behind the joke. The less your function does, the
more constrained it is, the less testing you have to do -- but the less
useful it is, and the more work you put onto the users of your function.
Instead of saying something like

a = MyNumericClass(1)
b = MyNumericClass(6)
# more code in here...
# ...
result = f(a, b)


you force them to do this:

a = MyNumericClass(1)
b = MyNumericClass(6)
# more code in here...
# ... 
# type-cast a and b to keep your function happy
result = f(int(a), int(b))
# and type-cast the result to what I want
result = MyNumericClass(result)

And that's assuming that they can even do that sort of type-cast without
losing too much information.


> Are other people doing things like this?  Any related commentary is
> welcome.

Generally speaking, type-checking is often just a way of saying "My
function could work perfectly with any number of possible types, but I
arbitrarily want it to only work with these few, just because."

Depending on what you're trying to do, there are lots of strategies for
avoiding type-tests: e.g. better to use isinstance() rather than type,
because that will accept subclasses. But it doesn't accept classes that
use delegation.

Sometimes you might have a series of operations, and you want the lot to
either succeed or fail up front, and not fail halfway through (say, you're
modifying a list and don't want to make half the changes needed). The
solution to that is to check that your input object has all the methods
you need:

def f(s):
    """Do something with a string-like object."""
    try:
        upper = s.upper
        split = s.split
    except AttributeError:
        raise TypeError('input is not sufficiently string-like')
    return upper()

Good unit tests will catch anything type and range tests will catch, plus
a whole lot of other errors, while type-testing and range-testing will
only catch a small percentage of bugs. So if you're relying on type- and
range-testing, you're probably not doing enough testing.


-- 
Steven.




More information about the Python-list mailing list