Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Exception proxy __str__ onto the args?

Why does printing an exception instance print the value of exc.args instead of representing exc directly? The docs call it a convenience but it's actually an inconvenience in practice.

Can't tell the difference between *args and a tuple:

>>> print(Exception(123, 456))
(123, 456)
>>> print(Exception((123, 456)))
(123, 456)

Can't reliably discern type:

>>> print(Exception('123'))
123
>>> print(Exception(123))
123

And the lovely "invisible" exception:

>>> print(Exception())

>>> 

Which you'll inherit unless you specifically ask not to:

>>> class MyError(Exception):
...     """an error in MyLibrary"""
...     
>>> print(MyError())

>>> 

This can be a real problem if you forget to log error instances specifically with repr - a default string representation in a log file has irreversibly lost information.

What's the rationale for such strange implementation of Exception.__str__? Presumably if a user wanted to print exc.args then they should just print exc.args?

like image 741
wim Avatar asked Oct 26 '18 18:10

wim


People also ask

What is args in exception in Python?

args attribute of exceptions is a tuple of all the arguments that were passed in (typically the one and only argument is the error message). This way you can modify the arguments and re-raise, and the extra information will be displayed. You could also put a print statement or logging in the except block.

How do you handle exceptions in Python?

If an exception occurs during execution of the try clause, the exception may be handled by an except clause. If the exception is not handled by an except clause, the exception is re-raised after the finally clause has been executed.

What is an OSError Python?

OSError is a built-in exception in Python and serves as the error class for the os module, which is raised when an os specific system function returns a system-related error, including I/O failures such as “file not found” or “disk full”.

How do you raise the HTTP exception in Python?

As a Python developer you can choose to throw an exception if a condition occurs. To throw (or raise) an exception, use the raise keyword.


1 Answers

BaseException.__str__ could have been fixed in a backwards-incompatible manner with Python 3 to include at least the type of the exception, but perhaps no one noticed that it is a thing that should be fixed.

The current implementation dates back to PEP 0352 which provides rationale:

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.

Of course Python itself breaks this principle of useful human-readable messages in many cases - for example stringification of a KeyError is the key that was not found, which leads to debug messages like

An error occurred: 42

The reason why str(e) is essentially str(e.args) or str(e.args[0]) was originally backwards-compatibility with Python 1.0. In Python 1.0, the syntax for raising an exception, such as ValueError would have been:

>>> raise ValueError, 'x must be positive'
Traceback (innermost last):
  File "<stdin>", line 1
ValueError: x must be positive

Python retained backwards-compatibility with 1.0 up to 2.7, so that you can run most Python 1.0 programs unchanged in Python 2.7 (like you never would):

>>> raise ValueError, 'x must be positive'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: x must be positive

Likewise, in Python 1.0 you would catch the ValueError with

>>> try:
...     raise ValueError, 'foo'
... except ValueError, e:
...     print 'Got ValueError', e

which worked unchanged in Python 2.7.

But the mechanism of how this worked internally had changed: In Python 1.0.1, ValueError was a string with value... 'ValueError'

>>> ValueError, type(ValueError)
('ValueError', <type 'string'>)

There were no exception classes at all, and you could only raise a single argument, or a tuple, with a string as a discriminator:

>>> class MyCustomException: 
...     pass
...   
>>> raise MyCustomException, 'my custom exception'
Traceback (innermost last):
  File "<stdin>", line 1
TypeError: exceptions must be strings

It would also be possible to give a tuple as an argument:

>>> raise ValueError, ('invalid value for x', 42)
Traceback (innermost last):
  File "<stdin>", line 1
ValueError: ('invalid value for x', 42)

And if you catch this "exception" in Python 1.0, what you get in e is:

>>> try:
...     raise ValueError, ('invalid value for x', 42)
... except ValueError, e:
...     print e, type(e)
... 
('invalid value for x', 42) 42 <type 'tuple'>

A tuple!

Let's try the code in Python 2.7:

>>> try:
...     raise ValueError, ('invalid value for x', 42)
... except ValueError, e:
...     print e, e[1], type(e)
... 
('invalid value for x', 42) 42 <type 'exceptions.ValueError'>

The output looks identical, except for the type of the value; which was a tuple before and now an exception... Not only does the Exception delegate __str__ to the args member, but it also supports indexing like a tuple does - and unpacking, iteration and so on:

Python 2.7

>>> a, b, c = ValueError(1, 2, 3)
>>> print a, b, c
1 2 3

All these hacks for the purpose of keeping backwards-compatibility.

The Python 2.7 behaviour comes from the BaseException class that was introduced in PEP 0352; PEP 0352 was originally implemented in Python 2.5.


In Python 3, the old syntax was removed - you could not raise exceptions with raise discriminator, (arg, um, ents); and the except could only use the Exception as e syntax.

PEP 0352 discussed about dropping support for multiple arguments to BaseException:

It was decided that it would be better to deprecate the message attribute in Python 2.6 (and remove it in Python 2.7 and Python 3.0) and consider a more long-term transition strategy in Python 3.0 to remove multiple-argument support in BaseException in preference of accepting only a single argument. Thus the introduction of message and the original deprecation of args has been retracted.

Seemingly this deprecation of args was forgotten, as it still does exist in Python 3.7 and is the only way to access the arguments given to many built-in exceptions. Likewise __str__ no longer needs to delegate to the args, and could actually alias the BaseException.__repr__ which gives nicer, unambiguous representation:

>>> BaseException.__str__(ValueError('foo', 'bar', 'baz'))
"('foo', 'bar', 'baz')"
>>> BaseException.__repr__(ValueError('foo', 'bar', 'baz'))
"ValueError('foo', 'bar', 'baz')"

but no one considered it.


P.S. The repr of an exception is useful - next time try printing your exception with !r format:

print(f'Oops, I got a {e!r}')

which results in

ZeroDivisionError('division by zero',)

being output.

like image 174