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