Using classes in the way you are doing is an abuse of an otherwise effective tool. You are not trying to encapsulate anything here. Your idea of having a separate method for each exception you want to transform is fine, but you are missing a way to look up the exception type.
The simplest way to edit the message of a builtin exception is to modify the args attribute:
try:
enumerate(1)
except Exception as e:
print(e.args)
e.args = (e.args[0].replace('not ', ''),)
raise
The result is
("'int' object is not iterable",)
Traceback (most recent call last):
File "<ipython-input-131-740297d24fb6>", line 1, in <module>
try: enumerate(1)
TypeError: 'int' object is iterable
The next step is to provide a method of identifying exceptions and mapping their messages. You can do something similar to what a series of except blocks does: provide a list of the exceptions you want to catch, in the order you want to catch them, and supply a callable to transform each one. Order is important, so you can either use a list, or a an OrderedDict. In python 3.6+, regular dicts are ordered, but I will not rely on this functionality here.
You can transform the exceptions in a number of ways. The callables can accept an exception object or a string. They can return an exception object or a string, or nothing. I would recommend ingesting an returning nothing. This is because raising a specific exception object in an except clause, even if it is the same exception that triggered the clause, will restart the traceback. Compare the snippet above with the one below:
try:
enumerate(1)
except Exception as e:
print(e.args)
e.args = (e.args[0].replace('not ', ''),)
raise e
Notice that the source of the error is no longer the enumerate line:
("'int' object is not iterable",)
Traceback (most recent call last):
File "<ipython-input-134-b1123bf70859>", line 6, in <module>
raise e
File "<ipython-input-134-b1123bf70859>", line 2, in <module>
enumerate(1)
TypeError: 'int' object is iterable
So you can write something like this:
from collections import OrderedDict
def handleIndexError(e):
e.args = ('You need to work on the size of your index',)
def handleMyCustomError(e):
e.args = (f'Really, be more careful: some attribute {e.attr} needs to be better',)
def handleBadException(e):
e.args = (f'This is a {type(e).__name__} error with message "{e.args[0]}"',)
handler_table = OrderedDict([
(IndexError, handleIndexError),
(MyCustomError, handleMyCustomError),
((MemoryError, OSError), handleBadException)])
def overwrite_exception(exception, table):
for cls, handler in table:
if isinstance(exception, cls):
return handler(exception)
try:
lis = []
a = lis[1]
except Exception as e:
overwrite_exception(e, handler_table)
raise
Notice that the OrderedDict does not give you much advantage over a list if you want to use isinstance functionality like the normal exceptions handlers do. If you want to match exact classes rather than using isinstance, you don't need ordering and a plain dict will do. In that case, you will have to list each type in a separate key. Normally, isinstance will accept a tuple, as in the last key of handler_table shown above.
The reason for having your handler accept the full exception object is that not all exceptions will use args for their str representation (or even have an args attribute. If you come across a type that uses a different mechanism to create the string representation, having the full exception object will allow you to adapt to it.
Also, don't forget about sys.exc_info, which can be used to glean information about the exception not available directly in the object itself.