[Python-ideas] Arguments to exceptions

Ken Kundert python-ideas at shalmirane.com
Sun Jul 2 15:19:54 EDT 2017


All,
    Here is a proposal for enhancing the way BaseException handles arguments.

-Ken


Rationale
=========

Currently, the base exception class takes all the unnamed arguments passed to an 
exception and saves them into args. In this way they are available to the 
exception handler.  A commonly used and very useful idiom is to extract an error 
message from the exception by casting the exception to a string. If you do so 
while passing one argument, you get that argument as a string:

    >>> try:
    ...     raise Exception('Hey there!')
    ... except Exception as e:
    ...     print(str(e))
    Hey there!

However, if more than one argument is passed, you get the string representation 
of the tuple containing all the arguments:

    >>> try:
    ...     raise Exception('Hey there!', 'Something went wrong.')
    ... except Exception as e:
    ...     print(str(e))
    ('Hey there!', 'Something went wrong.')

That behavior does not seem very useful, and I believe it leads to people 
passing only one argument to their exceptions.  An example of that is the system 
NameError exception:

    >>> 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. For example ...

    >>> import Food
    >>> try:
    ...     import meals

    ... except NameError as e:
    ...     name = str(e).split("'")[1]   # <-- fragile code
    ...     from difflib import get_close_matches
    ...     candidates = ', '.join(get_close_matches(name, Food.foods, 1, 0.6))
    ...     print(f'{name}: not found. Did you mean {candidates}?')

In this case, the missing name was needed but not directly available. Instead, 
the name must be extracted from the message, which is innately fragile.

The same is true with AttributeError: the only argument is a message and the 
name of the attribute, if needed, must be extracted from the message. Oddly, 
with a KeyError it is the opposite situation, the name of the key is the 
argument and no message is included. With IndexError there is a message but no 
index. However none of these behaviors can be counted on; they could be changed 
at any time.

When writing exception handlers it is often useful to have both a generic error 
message and access to the components of the message if for no other reason than 
to be able to construct a better error message.  However, I believe that the way 
the arguments are converted to a string when there are multiple arguments 
discourages this.  When reporting an exception, you must either give one 
argument or add a custom __str__ method to the exception.  To do otherwise means 
that the exception handlers that catch your exception will not have a reasonable 
error message and so would be forced to construct one from the arguments.

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.  However, 
the extra effort required means that in many cases people just pass an error 
message alone.  This approach is in effect discouraging people from adding 
additional arguments to exceptions, with the result being that if they are 
needed by the handler they have to be extracted from the message.  It is 
important to remember that the person that writes the exception handler often 
does not raise the exception, and so they just must live with what is available.  
As such, a convention that encourages the person raising the exception to 
include all the individual components of the message should be preferred.

That is the background. Here is my suggestion on how to improve this situation.


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:

    class BaseException:
        def __init__(self, *args, **kwargs):
            self.args = args
            self.kwargs = kwargs

        def __str__(self):
            template = self.kwargs.get('template')
            if template is None:
                sep = self.kwargs.get('sep', ' ')
                return sep.join(str(a) for a in self.args)
            else:
                return template.format(*self.args, **self.kwargs)


Benefits
========

Now, NameError could be defined as:

    class NameError(Exception):
        pass

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)
        ...

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)
        ...

One of the nice benefits of this approach is that the message printed can be 
easily changed after the exception is caught. For example, it could be converted 
to German.

    try:
        raise NameError(name, template="name '{0}' is not defined.")
    except NameError as e:
        print('{}: nicht gefunden.'.format(e.args[0]))

A method could be provided to generate the error message from a custom format 
string:

    try:
        raise NameError(name, template="name '{0}' is not defined.")
    except NameError as e:
        print(e.render('{}: nicht gefunden.'))

Another nice benefit of this approach is that both named and unnamed arguments 
to the exception are retained and can be processed by the exception handler.  
Currently this is only true of unnamed arguments. And since named arguments are 
not currently allowed, this proposal is completely backward compatible.

Of course, with this change, the built-in exceptions should be changed to use 
this new approach. Hopefully over time, others will change the way they write 
exceptions to follow suit, making it easier to write more capable exception 
handlers.


Conclusion
==========

Sorry for the length of the proposal, but I thought it was important to give 
a clear rationale for it. Hopefully me breaking it into sections made it easier 
to scan.

Comments?


More information about the Python-ideas mailing list