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:
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AssertionError
This is better:
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:
.... 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.