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? _______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/