[Tutor] custom error classes: how many of them does one need?

Steven D'Aprano steve at pearwood.info
Fri Jul 5 04:20:07 CEST 2013


On 05/07/13 05:40, Albert-Jan Roskam wrote:
> Hello,
>
> I created a custom error class like under [A] below (see http://pastebin.com/JLwyPsRL if you want to see highlighted code). Now I am thinking: is this specific enough? "When
> creating a module that can raise several distinct errors, a common practice is
> to create a base class for exceptions defined by that module, and subclass that
> to create specific exception classes for different error conditions" (http://docs.python.org/2/tutorial/errors.html)
> Given that I have many retcodes, is it a better approach to define a class factory that generates a custom exception for each retcode on-the-fly? (under [B]). Using a class factory seems a little unusual, or isn it? More generally: what criteria should one follow when distinguishing error classes?

Return codes are not exceptions, and generally speaking using return codes for errors is *strongly* discouraged unless you have a language without exceptions. (Although the designers of Go language disagree. I think they are wrong to do so.) Although looking forward, I see that even though you call them "retcodes", you don't actually use them as return codes.


> global retcodes
> retcodes = {0: "OK", 1: "Error_X", 2: "Error_Y"}

You don't need to declare a global variable global in the global part of your file. Although Python allows it, it is redundant and rather silly. Delete the line "global retcodes".


> #############################
> # [A] one custom exception for all purposes, but with a descriptive message
> #############################
>
> class single_custom_exception(Exception):
>      def __init__(self, retcode, message):
>          self.retcode = retcode
>          message += " [%s]" % retcodes.get(retcode)
>          Exception.__init__(self, message)

Exceptions are classes, and like all classes, the convention is to name them in CamelCase.


> #############################
> # [B] generating custom exceptions with a class factory
> #############################
>
> class BaseError(Exception): pass
>
> def throw(retcode, message):
>      """class factory that generates an error class for
>      each return code label"""
>      class custom_exception(BaseError):
>          def __init__(self):
>              BaseError.__init__(self, message)
>      exception = retcodes.get(retcode)
>      custom_exception.__name__ = exception
>      return custom_exception

This is overkill. No need to create a custom class *on the fly* for each return code. Doing so is pointless: the caller *cannot* catch them individually, because they are created on the fly. So they can only catch BaseError, then inspect the exception's details. If you have to do that, then having individual classes is a waste of time. You might as well just use a single class and be done with it.

What you would like to do, but cannot, since neither Error_X nor Error_Y are defined:

try:
     code_that_might_fail()
except Error_X:
     handle_error_X
except Error_Y:
     handle_error_Y



What you are forced to do:

try:
     code_that_might_fail()
except BaseError as err:
     if err.retcode == 0:
         print ("Error: no error occurred!")
     elif err.retcode == 1:
         handle_error_X
     elif err.retcode == 1:
         handle_error_Y
     else:
         print ("Error: an unexpected error occurred!")


which is ugly enough, but it makes the class factory redundant.

So, how should you handle this scenario?

Use a separate exception class for each high-level *category* of errors. E.g. Python has:

TypeError
ValueError
ZeroDivisionError
UnicodeEncodeError

etc. Within your app, can you divide the return codes into application-specific groups? If not, then you just need a single exception class:

MyApplicationError


If you do have a bunch of application-specific groups, then you might develop a hierarchical family of exceptions:


WidgetError
+-- WrongWidgetTypeError
+-- InvalidWidgetError
+-- WidgetNotInitialisedError


WidgetGroupError
+-- MissingWidgetError
|   +-- MissingCriticalWidgetError
+-- TooManyWidgetsError


SpoonError
+-- TeaspoonError
+-- SoupSpoonError
+-- DesertSpoonError


The parent exceptions, WidgetError, WidgetGroupError and SpoonError do not need to share a common base class.

This hierarchy might completely obliterate the need for return codes. Python's build-in exceptions generally do not. However, maybe you do still need them, similar to the way Python's OSError and IOError have error codes. Assuming you do:


def someFunc():
     perform_calculations()
     if there are no spoons:
         raise TeaspoonError("cannot stir my tea")
     elif some other error:
         raise InvalidWidgetError(retcode=99, message="Can't use red widget")
     else:
         return some value



-- 
Steven


More information about the Tutor mailing list