Proper way to declare custom exceptions in modern Python?
With modern Python Exceptions, you don't need to abuse .message
, or override .__str__()
or .__repr__()
or any of it. If all you want is an informative message when your exception is raised, do this:
class MyException(Exception):
pass
raise MyException("My hovercraft is full of eels")
That will give a traceback ending with MyException: My hovercraft is full of eels
.
If you want more flexibility from the exception, you could pass a dictionary as the argument:
raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
However, to get at those details in an except
block is a bit more complicated. The details are stored in the args
attribute, which is a list. You would need to do something like this:
try:
raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
details = e.args[0]
print(details["animal"])
It is still possible to pass in multiple items to the exception and access them via tuple indexes, but this is highly discouraged (and was even intended for deprecation a while back). If you do need more than a single piece of information and the above method is not sufficient for you, then you should subclass Exception
as described in the tutorial.
class MyError(Exception):
def __init__(self, message, animal):
self.message = message
self.animal = animal
def __str__(self):
return self.message
see how exceptions work by default if one vs more attributes are used (tracebacks omitted):
>>> raise Exception('bad thing happened')
Exception: bad thing happened
>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')
so you might want to have a sort of "exception template", working as an exception itself, in a compatible way:
>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened
>>> raise nastyerr()
NastyError: bad thing happened
>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')
this can be done easily with this subclass
class ExceptionTemplate(Exception):
def __call__(self, *args):
return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass
and if you don't like that default tuple-like representation, just add __str__
method to the ExceptionTemplate
class, like:
# ...
def __str__(self):
return ': '.join(self.args)
and you'll have
>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken
"What is the proper way to declare custom exceptions in modern Python?"
This is fine unless your exception is really a type of a more specific exception:
class MyException(Exception):
pass
Or better (maybe perfect), instead of pass
give a docstring:
class MyException(Exception):
"""Raise for my specific kind of exception"""
Subclassing Exception Subclasses
From the docs
Exception
All built-in, non-system-exiting exceptions are derived from this class. All user-defined exceptions should also be derived from this class.
That means that if your exception is a type of a more specific exception, subclass that exception instead of the generic Exception
(and the result will be that you still derive from Exception
as the docs recommend). Also, you can at least provide a docstring (and not be forced to use the pass
keyword):
class MyAppValueError(ValueError):
'''Raise when my specific value is wrong'''
Set attributes you create yourself with a custom __init__
. Avoid passing a dict as a positional argument, future users of your code will thank you. If you use the deprecated message attribute, assigning it yourself will avoid a DeprecationWarning
:
class MyAppValueError(ValueError):
'''Raise when a specific subset of values in context of app is wrong'''
def __init__(self, message, foo, *args):
self.message = message # without this you may get DeprecationWarning
# Special attribute you desire with your Error,
# perhaps the value that caused the error?:
self.foo = foo
# allow users initialize misc. arguments as any other builtin Error
super(MyAppValueError, self).__init__(message, foo, *args)
There's really no need to write your own __str__
or __repr__
. The built-in ones are very nice, and your cooperative inheritance ensures that you use them.
Critique of the top answer
Maybe I missed the question, but why not:
class MyException(Exception):
pass
Again, the problem with the above is that in order to catch it, you'll either have to name it specifically (importing it if created elsewhere) or catch Exception, (but you're probably not prepared to handle all types of Exceptions, and you should only catch exceptions you are prepared to handle). Similar criticism to the below, but additionally that's not the way to initialize via super
, and you'll get a DeprecationWarning
if you access the message attribute:
Edit: to override something (or pass extra args), do this:
class ValidationError(Exception):
def __init__(self, message, errors):
# Call the base class constructor with the parameters it needs
super(ValidationError, self).__init__(message)
# Now for your custom code...
self.errors = errors
That way you could pass dict of error messages to the second param, and get to it later with e.errors
It also requires exactly two arguments to be passed in (aside from the self
.) No more, no less. That's an interesting constraint that future users may not appreciate.
To be direct - it violates Liskov substitutability.
I'll demonstrate both errors:
>>> ValidationError('foo', 'bar', 'baz').message
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)
>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'
Compared to:
>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Maybe I missed the question, but why not:
class MyException(Exception):
pass
To override something (or pass extra args), do this:
class ValidationError(Exception):
def __init__(self, message, errors):
# Call the base class constructor with the parameters it needs
super().__init__(message)
# Now for your custom code...
self.errors = errors
That way you could pass dict of error messages to the second param, and get to it later with e.errors
.
In Python 2, you have to use this slightly more complex form of super()
:
super(ValidationError, self).__init__(message)