those darn exceptions

Ben Finney ben+python at benfinney.id.au
Tue Jun 21 00:04:00 EDT 2011


Chris Torek <nospam at torek.net> writes:

> It can be pretty obvious. For instance, the os.* modules raise OSError
> on errors.

Not *only* OSError, of course.

> The examples here are slightly silly until I reach the "real" code at
> the bottom, but perhaps one will get the point:
>
>     >>> import os
>     >>> os.kill(getpid(), 0) # am I alive?
>     >>> # yep, I am alive.
>     ...
>
> [I'm not sure why the interpreter wants more after my comment here.]

Because it's waiting for the end of the statement. (A comment is ignored
by the compiler, so doesn't become part of the statement.)

For a no-op statement, use ‘pass’::

    >>> pass  # yep, I am alive.
    >>>

> So now I am ready to write my "is process <pid> running" function:
>
>     import os, errno
>
>     def is_running(pid):
>         "Return True if the given pid is running, False if not."
>         try:
>             os.kill(pid, 0)
>         except OSError, err:

You should avoid ambiguity (is this two types of exception, or two
arguments?) by using the new syntax for an ‘except’ clause::

            except OSError as err:

>             # We get an EPERM error if the pid is running
>             # but we are not allowed to signal it (even with
>             # signal 0).  If we get any other error we'll assume
>             # it's not running.

Why assume it's not running? You get a specific error for that case,
‘errno.ESRCH’. That's what you should be comparing

>             if err.errno != errno.EPERM:
>                 return False

You've caught the OSError, but you're throwing it away here. You've done
nothing with the exception and it's swallowed silently by your code.

Instead, if you realise you don't want to handle the exception, you need
to re-raise it so it propagates, using ‘raise’ with no arguments.

Here's my suggestion for improving that code::

        import os
        import signal
        import errno

        def is_running(pid):
            """ Return True iff the specified PID is running. """

            try:
                os.kill(pid, signal.SIG_DFL)
            except OSError as err:
                if err.errno == errno.EPERM:
                    # We get an EPERM error if the pid is running
                    # but we are not allowed to signal it (even with
                    # signal 0).
                    result = True
                if err.errno == errno.ESRCH:
                    # The process ID wasn't found by ‘os.kill’.
                    result = False
                else:
                    # If we get any other OSError we aren't handling it;
                    # re-raise it up the call stack.
                    raise
            else:
                # No exception was raised.
                result = True

            return result

> This function works great, and never raises an exception itself.

“Never raises an exception” is a poor design goal. The point of
exceptions is that they allow the programmer to handle them where it
makes most sense. Swallowing them silently is a code smell, as in this
case.

> Oops!  It turns out that os.kill() can raise OverflowError (at
> least in this version of Python, not sure what Python 3.x does).
>
> Now, I could add, to is_running, the clause:
>
>         except OverflowError:
>             return False

That would be a bad idea. An OverflowError is exactly what the caller
needs to know: the number specified was too big. There's little point in
catching it yourself, only to lose the information so the caller can't
know what the problem was.

> But how can I know a priori that os.kill() could raise OverflowError
> in the first place?

By designing your unit test cases well.

> It would be better just to note somewhere that OverflowError is
> one of the errors that os.kill() "normally" produces (and then,
> presumably, document just when this happens, so although having
> noted that it can, one could make an educated guess).

Agreed. I'm sure the Python documentation maintainers would appreciate a
patch to the documentation, submitted in the proper place: the Python
bug tracker <URL:http://bugs.python.org/>.

> Functions have a number of special "__" attributes.

That convention is for attributes treated as special by Python itself.
It's an indicator “this attribute will change the way Python uses this
object, without necessarily seeing this attribute used by this name in
any of my code”.

> I think it might be reasonable to have all of the built-in functions,
> at least, have one more, perhaps spelled __exceptions__, that gives
> you a tuple of all the exceptions that the function might raise.

There's no such set smaller than the entire set of all exceptions. Any
code might call any other, which can then raise any exception.

>     >>> os.kill.__exceptions__
>     (<type 'exceptions.OSError'>, <type 'exceptions.TypeError'>, <type 'exceptions.OverflowError'>, <type 'exceptions.DeprecationWarning'>)

This is false. Any exception could be raised by any call. Learn to live
with that.

-- 
 \     “Computers are useless. They can only give you answers.” —Pablo |
  `\                                                           Picasso |
_o__)                                                                  |
Ben Finney



More information about the Python-list mailing list