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
?
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.
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.
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”.
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.
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-1args
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 inBaseException
in preference of accepting only a single argument. Thus the introduction of message and the original deprecation ofargs
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With