It isn’t wonderfully well documented in Python that the
builtin Exception
class takes any number of positional arguments (pargs) and uses them to provide __str__()
and
__repr__()
methods and pickling support. If you understand how Exception
‘s pargs work then you can use it as a base
class correctly and inherit these features for your custom exception classes.
Exception
with a single parg
If you give Exception
a single parg, for example an error message string, this string become’s the exception’s informal
string representation (the BaseException.__str__()
method):
>>> e = Exception("Something went wrong")
>>> print(e)
Something went wrong
__str__()
is meant to return a concise and convenient string representation of the object,
it’s called by the print()
and format()
builtins.
The parg string will also be used along with the class name in the exception’s formal string representation (the
BaseException.__repr__()
method):
>>> e = Exception("Something went wrong")
>>> repr(e)
>>> "Exception('Something went wrong')"
__repr__()
is meant to return an information-rich and unambiguous string representation of the object
for debugging. Whenever possible it’s supposed to be a valid expression for recreating the object with the same value,
that’s why it looks like Python code for creating a new Exception
.
Exception
with multiple pargs
You can pass any number of pargs to Exception
. If you pass it more than one parg then Exception.__str__()
and
Exception.__repr__()
change to using a tuple representation of the pargs. The pargs don’t all have to be strings:
>>> e = Exception("Something Error", 42, "Something went wrong")
>>> print(e)
('Something Error', 42, 'Something went wrong')
>>> repr(e)
"Exception('Something Error', 42, 'Something went wrong')"
Note that __str__()
returns a string representation of a tuple, not an actual tuple. The pargs are also readable from
the Exception.args
property, which is an actual tuple. args
is always a tuple regardless of whether one or more
than one parg was given:
>>> e.args
('Something Error', 42, 'Something went wrong')
Exception
doesn’t accept any kwargs
Exception
does not accept any keyword arguments (kwargs):
>>> e = Exception(error_message="Something went wrong")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Exception() takes no keyword arguments
Custom exception classes inherit __str__()
and __repr__()
If you write your own Exception
subclass then you get the variable number of __init__()
pargs and the __str__()
and
__repr__()
for free. This is Python giving us a hint that most exception classes should work with a small number of
positional arguments, rather than keyword arguments:
>>> class MyError(Exception):
... """My custom error class."""
...
>>> e = MyError(500, "Internal Server Error")
>>> print(e)
(500, 'Internal Server Error')
>>> repr(e)
"MyError(500, 'Internal Server Error')"
>>> e.args
(500, 'Internal Server Error')
If you want to name your custom error class’s pargs, and force every instance of it to have the same number of pargs, then
you can add an __init__()
method:
>>> class MyError(Exception):
... def __init__(self, status_code, reason):
... pass
...
>>> e = MyError(500, "Internal Server Error")
>>> print(e)
(500, 'Internal Server Error')
>>> repr(e)
"MyError(500, 'Internal Server Error')"
>>> e.args
(500, 'Internal Server Error')
Note that I didn’t do anything with status_code
or reason
(for example I didn’t save them on self
), and I didn’t call
super().__init__()
, and this still worked.
You need to call super().__init__()
Pylint will complain that you didn’t call the base class’s __init__()
and for good reason: by naming the __init__()
arguments we’ve enabled user code to pass them to MyError
as kwargs rather than pargs, and this breaks BaseException
‘s
args
, __str__()
and __repr__()
:
>>> e = MyError(status_code=500, reason="Internal Server Error")
>>> print(e)
>>> repr(e)
'MyError()'
>>> e.args
()
Exception
isn’t meant to accept kwargs, and as we saw above you can’t pass kwargs to Exception
directly, but with our
custom __init__()
we’ve enabled them.
Fortunately this is easy to fix – just call super().__init__()
as Pylint wants us to and pass the arguments to it as
pargs. Let’s also provide easy access to status_code
and reason
(e.status_code
instead of e.args[0]
) by adding them
to self
:
>>> class MyError(Exception):
... def __init__(self, status_code, reason):
... super().__init__(status_code, reason)
... self.status_code = status_code
... self.reason = reason
...
>>> e = MyError(status_code=500, reason="Internal Server Error")
>>> print(e)
(500, 'Internal Server Error')
>>> repr(e)
"MyError(500, 'Internal Server Error')"
>>> e.args
(500, 'Internal Server Error')
>>> e.status_code
500
>>> e.reason
'Internal Server Error'
Now we have a custom exception class with a fixed number of named arguments and a nice __str__()
and __repr__()
inherited from Exception
. Of course you can still pass status_code
and reason
as pargs as well:
MyError(500, "Internal Server Error")
.
The BaseException
source code is better than the docs
The C source code for BaseException
contains the implementation of the pargs-based __str__()
and __repr__()
methods, as well as other BaseException
properties such as __cause__
(the other exception that the exception was raised from, if
raise SomeError from other_error
was used) and pickling support.