raise None

Steven D'Aprano steve at pearwood.info
Mon Jan 4 00:19:51 EST 2016


On Fri, 1 Jan 2016 09:48 am, Chris Angelico wrote:

> On Fri, Jan 1, 2016 at 7:18 AM, Ben Finney <ben+python at benfinney.id.au>
> wrote:
[...]
>> As best I can tell, Steven is advocating a way to obscure information
>> from the traceback, on the assumption the writer of a library knows that
>> I don't want to see it.
>>
>> Given how very often such decisions make my debugging tasks needlessly
>> difficult, I'm not seeing how that's a desirable feature.
> 
> What Steven's actually advocating is removing a difference between
> Python code and native code. Compare:

Well, not quite. What I'm really doing is two-fold:

(1) reminding people that the part of the code which determines the
existence of an error need not be the part of the code which actually calls
raise; and

(2) suggesting a tiny change to the semantics of raise which would make this
idiom easier to use. (Namely, have "raise None" be a no-op.)

I'm saddened but not astonished at just how much opposition there is to
point (1), even though it is something which Python has been capable of
since the earliest 1.x days. Exceptions are first-class objects, and just
because raising an exception immediately after a test is a common idiom:

    if condition:
        raise SomeError('whatever')

doesn't mean it is *always* the best idiom. I have identified a common
situation in my own code where I believe that there is a better idiom. From
the reaction of others, one might think I've suggested getting rid of
exceptions altogether and replacing them with GOTO :-)

Let's step back a bit and consider what we might do if Python were a less
capable language. We might be *forced* to perform error handling via status
codes, passed from function to function as needed, until we reach the top
level of code and either print the error code or the program's intended
output. None of us want that, but maybe there are cases where a less
extreme version of the same thing is useful. Just because I detect an error
condition in one function doesn't necessarily mean I want to trigger an
exception at that point. Sometimes it is useful to delay raising the
exception.

Suppose I write a validation function that returns a status code, perhaps an
int, or a Enum:


def _validate(arg):
    if condition(arg):
        return CONDITION_ERROR
    elif other_condition(arg):
        return OTHER_ERROR
    return SUCCESS

def func(x):
    status = _validate(x)
    if status == CONDITION_ERROR:
        raise ConditionError("condition failed")
    elif status == OTHER_ERROR:
        raise OtherError("other condition failed")
    assert status == SUCCESS
    ...


Don't worry about *why* I want to do this, I have my reasons. Maybe I want
to queue up a whole bunch of exceptions before doing something with them,
or conditionally decide whether or not raise, like unittest. Perhaps I can
do different sorts of processing of different status codes, including
recovery from some:

def func(x):
    status = _validate(x)
    if status == CONDITION_ERROR:
        warnings.warn(msg)
        x = massage(x)
        status = SUCCESS
    elif status == OTHER_ERROR:
        raise SomeError("an error occurred")
    assert status == SUCCESS
    do_the_real_work(x)


There's no reason to say that I *must* raise an exception the instant I see
a problem.

But why am I returning a status code? This is Python, not C or bash, and I
have a much more powerful and richer set of values to work with. I can
return an error type, and an error message:


def _validate(arg):
    if condition(arg):
        return (ConditionError, "condition failed")
    elif other_condition(arg):
        return (OtherError, "something broke")
    return None


But if we've come this far, why mess about with tuples when we have an
object oriented language with first class exception objects?


def _validate(arg):
    if condition(arg):
        return ConditionError("condition failed")
    elif other_condition(arg):
        return OtherError("something broke")
    return None



The caller func still decides what to do with the status code, and can
process it when needed. If the error is unrecoverable, it can raise. In
that case, the source of the exception is func, not _validate. Just look at
the source code, it tells you right there where the exception comes from:

def func(x):
    exc = _validate(x)
    if exc is not None:
        raise exc  #  <<<< this is the line you see in the traceback
    do_the_real_work(x)


This isn't "hiding information", but it might be *information hiding*, and
it is certainly no worse than this:

def spam():
    if condition:
        some_long_message = "something ...".format(
                many, extra, arguments)
        exc = SomeError(some_long_message, data)
        raise exc   #  <<<< this is the line you see in the traceback


If I have a lot of functions that use the same exception, I can refactor
them so that building the exception object occurs elsewhere:

def spam():
    if condition:
        exc = _build_exception()
        raise exc   #  <<<< this is STILL the line you see in the traceback

and likewise for actually checking the condition:


def spam():
    exc = _validate_and_build_exception()
    if exc is not None:
        raise exc   #  <<<<<<<<<<<<


Fundamentally, _validate is an implementation detail. The semantics of func
will remain unchanged whether it does error checking inside itself, or
passes it off to another helper function. The very existence of the helper
function is *irrelevant*. We have true separation of concerns:

(1) _validate decides whether some condition (nominally an error 
    condition) applies or not;

(2) while the caller func decides whether it can recover from 
    that error or needs to raise.


(Aside: remember in my use-case I'm not talking about a single caller func.
There might be dozens of them.)

If func decides it needs to raise, the fact that _validate made the decision
that the condition applies is irrelevant. The only time it is useful to see
_validate in the traceback is if _validate fails and raises an exception
itself.




-- 
Steven




More information about the Python-list mailing list