List comprehension for testing **params

Steven D'Aprano steve+comp.lang.python at pearwood.info
Mon Nov 12 02:32:02 EST 2012


On Sun, 11 Nov 2012 18:21:32 -0600, Tim Chase wrote:

> On 11/11/12 17:18, Steven D'Aprano wrote:
>> but that leaves you with the next two problems:
>> 
>> 2) Fixing the assert still leaves you with the wrong exception. You
>> wouldn't raise a ZeroDivisionError, or a UnicodeDecodeError, or an
>> IOError would you? No of course not. So why are you suggesting raising
>> an AssertionError? That is completely inappropriate, the right
>> exception to use is TypeError. Don't be lazy and treat assert as a
>> quick and easy way to get exceptions for free.
> 
> I'd say that it depends on whether the dictionary/kwargs gets populated
> from user-supplied data (whether mail, a file, direct input, etc), or
> whether it's coder-supplied.

No, it doesn't matter if it is end-user supplied or coder-supplied. As 
usual, look at the standard library and Python builtins for best 
practices:

py> len(42)  # do we get an AssertionError?
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()



> I like to think of "assert" as a "hey, you bone-headed programmer, you
> did something that violated conditions for using this code properly!"

You mean like passing a list to ord()? Or trying to add a number and a 
string?

Raise the appropriate exception for the error: TypeError for type errors 
and missing function parameters, ValueErrors for objects of the right 
type but a bad argument, etc. Assertion errors are for asserting facts 
about internal program logic. "I assert that such-and-such is a fact", 
rather than "I require that such-and-such is a fact".

For example, this might be a reasonable (although trivial) use of assert:

def spam(n):
    if isinstance(n, int) and n > 0:
        result = ' '.join(["spam"]*n)
        assert result, "expected non-empty string but got empty"
        return result
    else:
        raise SpamError("No spam for you!!!")

This gives a measure of protection against logic errors such as writing 
n >= 0.

The presence of the assert is partly documentation and partly error 
checking, but the important thing is that it checks things which depend 
on the internal logic of the code itself, not on the input to the code. 
Given that you have already explicitly tested that n > 0, then the 
logical consequence is that "spam"*n will be non-empty. Hence an 
assertion "I assert that the string is not empty", rather than an overt 
test "Is the string empty?"

Now, I accept that sometimes it is hard to draw a line between "internal 
program logic" and external input. E.g. if one function generates a value 
x, then calls another function with x as argument, the state of x is an 
internal detail to the first function and an external input to the other 
function. The rules I use are:

* If the argument is a public parameter to a public function, 
  then you must not use assert to validate it;

* If the argument is a private parameter, or if the function 
  is a private function, then you may use assert to validate
  it (but probably shouldn't);

* You can use assert to check conditions of function internal
  variables (locals that you control);

* Once you cross the boundary to another function, your objects
  should be treated as external again.

E.g. I might have:

def foo(a, b):
    # foo, a and b are public, so don't use assert
    if not (isinstance(a, int) and isinstance(b, int)):
        raise TypeError
    x = abs(a) + abs(b)
    # x is purely internal, so okay to assert
    assert x >= 0, "Hey McFly, you screwed up!"
    return bar(x)

def bar(x):
    if x <= 0:
        raise ValueError("expected positive x")
    ...

This now guards against something other than foo calling bar.



-- 
Steven



More information about the Python-list mailing list