raise None

Steven D'Aprano steve at pearwood.info
Thu Dec 31 12:50:55 EST 2015


On Fri, 1 Jan 2016 03:46 am, Oscar Benjamin wrote:

[...]
> Exactly. The critical technique is looking at the traceback and splitting
> it between what's your code and what's someone else's. Hopefully you don't
> need to look at steves_library.py to figure out what you did wrong.
> However if you do need to look at Steve's code you're now stumped because
> he's hidden the actual line that raises. All you know now is that
> somewhere in _validate the raise happened. Why hide that piece of
> information and complicate the general interpretation of stack traces?

No. I don't hide anything. Here's a simple example, minus any hypothetical
new syntax, showing the traditional way and the non-traditional way.


# example.py
def _validate(arg):
    if not isinstance(arg, int):
        # traditional error handling: raise in the validation function
        raise TypeError('expected an int')
    if arg < 0:
        # non-traditional: return and raise in the caller
        return ValueError('argument must be non-negative')

def func(x):
    exc = _validate(x)
    if exc is not None:
        raise exc
    print(x+1)

def main():
    value = None  # on the second run, edit this to be -1
    func(value)

main()




And here's the traceback you get in each case. First, the traditional way,
raising directly inside _validate:


[steve at ando tmp]$ python example.py
Traceback (most recent call last):
  File "example.py", line 17, in <module>
    main()
  File "example.py", line 15, in main
    func(value)
  File "example.py", line 8, in func
    exc = _validate(x)
  File "example.py", line 3, in _validate
    raise TypeError('expected an int')
TypeError: expected an int


What do we see? Firstly, the emphasis is on the final call to _validate,
where the exception is actually raised. (As it should be, in the general
case where the exception is an error.) If you're like me, you're used to
skimming the traceback until you get to the last entry, which in this case
is:

    File "example.py", line 3, in _validate

and starting to investigate there. But that's a red herring, because
although the exception is raised there, that's not where the error lies.
_validate is pretty much just boring boilerplate that validates the
arguments -- where we really want to start looking is the previous entry,
func, and work backwards from there.

The second thing we see is that the displayed source code for _validate is
entirely redundant:

    raise TypeError('expected an int')

gives us *nothing* we don't see from the exception itself:

    TypeError: expected an int

This is a pretty simple exception. In a more realistic example, with a
longer and more detailed message, you might see something like this as the
source extract:

    raise TypeError(msg)


where the message is set up in the previous line or lines. This is even less
useful to read.

So it is my argument that the traditional way of refactoring parameter
checks, where exceptions are raised in the _validate function, is
sub-optimal. We can do better.

Here's the traceback we get from the non-traditional error handling. I edit
the file to change the value = None line to value = -1 and re-run it:

[steve at ando tmp]$ python example.py
Traceback (most recent call last):
  File "example.py", line 17, in <module>
    main()
  File "example.py", line 15, in main
    func(value)
  File "example.py", line 10, in func
    raise exc
ValueError: argument must be non-negative


Nothing is hidden. We still see the descriptive exception and error message,
and the line

    raise exc

is no worse than "raise TypeError(msg)" -- all the detail we need is
immediately below it. 

The emphasis here is on the call to func, since that's the last entry in the
call stack. The advantage is that we don't see the irrelevant call to
_validate *unless we go looking for it in the source code*. We start our
investigate where we need to start, namely in func itself.

Of course, none of this is mandatory, nor is it new. Although I haven't
tried it, I'm sure that this would work as far back as Python 1.5, since
exceptions are first-class values that can be passed around and raised when
required. It's entirely up to the developer to choose whether this
non-traditional idiom makes sense for their functions or not. Sometimes it
will, and sometimes it won't.

The only new part here is the idea that we could streamline the code in the
caller if "raise None" was a no-op. Instead of writing this:


    exc = _validate(x)
    if exc is not None:
        raise exc


we could write:

    raise _validate(x)

which would make this idiom more attractive.



-- 
Steven




More information about the Python-list mailing list