[Python-ideas] Arguments to exceptions

Steven D'Aprano steve at pearwood.info
Sun Jul 2 23:18:10 EDT 2017


On Sun, Jul 02, 2017 at 12:19:54PM -0700, Ken Kundert wrote:

[...]
>     >>> try:
>     ...     foo
>     ... except Exception as e:
>     ...     print('str:', str(e))
>     ...     print('args:', e.args)
>     str: name 'foo' is not defined
>     args: ("name 'foo' is not defined",)
> 
> Notice that the only argument is the error message. If you want access to the 
> problematic name, you have to dig it out of the message.

In the common case, you don't. You know the name because you know what 
name you just tried to look up:

try:
    spam
except NameError:
    name = "spam"  # what else could it be?
    print("%s missing" % name)


The general rule for try...except is that the try block should contain 
only the smallest amount of code that can fail, that you can deal with. 
So if it is important for your code to distinguish *which* name failed, 
you should put them in separate try blocks:

# don't do this
try:
    spam or eggs or cheese or aardvark
except NameError as err:
    name = err.name  # doesn't actually work
    if name == 'spam':
        spam = 'hello'
    elif name == 'eggs':
        eggs = 'hello' 
    elif ... # you get the picture


One problem with that is that it assumes that only *one* name might not 
exist. If the lookup of spam failed, that doesn't mean that eggs would 
have succeeded. Instead, we should write:

# do this
try:
    spam
except NameError:
    spam = "hello"

try:
    eggs
except NameError:
    ... # and so on


I won't categorically state that it is "never" useful to extract the 
name from NameError (or key from KeyError, index from IndexError, etc) 
but I'd consider that needing to do so programmatically may be a code 
smell: something which may be fine, but it smells a little fishy and 
requires a closer look.


[...]
> This is spelled out in PEP 352, which explicitly recommends that there be only 
> one argument and that it be a helpful human readable message. Further it 
> suggests that if more than one argument is required that Exception should be 
> subclassed and the extra arguments should be attached as attributes.


    No restriction is placed upon what may be passed in for args for 
    backwards-compatibility reasons. In practice, though, only a single 
    string argument should be used. This keeps the string representation 
    of the exception to be a useful message about the exception that is 
    human-readable; this is why the __str__ method special-cases on 
    length-1 args value. Including programmatic information (e.g., an 
    error code number) should be stored as a separate attribute in a 
    subclass.

https://www.python.org/dev/peps/pep-0352/


> Proposal
> ========
> 
> I propose that the Exception class be modified to allow passing a message 
> template as a named argument that is nothing more than a format string that 
> interpolates the exception arguments into an error message. If the template is 
> not given, the arguments would simply be converted to strings individually and 
> combined as in the print function. So, I am suggesting the BaseException class 
> look something like this:

[snip example implementation]

> A NameError would be raised with:
> 
>     try:
>         raise NameError(name, template="name '{0}' is not defined.")
>     except NameError as e:
>         name = e.args[0]
>         msg = str(e)
>         ...

I think that's *exactly* what PEP 352 is trying to get away from: people 
having to memorize the order of arguments to the exception, so they know 
what index to give to extract them.

And I tend to agree. I think that's a poor API. Assuming I ever find a 
reason to extract the name, I don't want to have to write 
`error.args[0]` to do so, not if I can write `error.name` instead.

Look at ImportError and OSError: they define individual attributes for 
important error information:

py> dir(OSError)
[  # uninterest dunders trimmed ...
'args', 'characters_written', 'errno', 'filename', 'filename2', 
'strerror', 'with_traceback']


> Or, perhaps like this:
> 
>     try:
>         raise NameError(name=name, template="name '{name}' is not defined.")
>     except NameError as e:
>         name = e.kwargs['name']
>         msg = str(e)
>         ...


That's not much of an improvement.

Unless you can give a good rationale for why PEP 353 is wrong to 
recommend named attributes instead of error.args[index], I think this 
proposal is the wrong approach.



-- 
Steve


More information about the Python-ideas mailing list