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

Albert-Jan Roskam fomcl at yahoo.com
Sun Jul 7 20:37:13 CEST 2013



----- Original Message -----

> From: Steven D'Aprano <steve at pearwood.info>
> To: tutor at python.org
> Cc: 
> Sent: Friday, July 5, 2013 4:20 AM
> Subject: Re: [Tutor] custom error classes: how many of them does one need?
> 
> 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.

Hi Steven, Alan, Eryksun,

In my app, the return codes are from C functions. < 0 means warning, 0 means OK, > 0 means error.
You're right that I was sloppy as I used the terms retcodes and exceptions interchangably.

> 
>>  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".

Oops, despite of an earlier thread I still did this wrong. I used to think that "global" made The Unforgiveable Thing Of All Times (using a global variable) less bad because at least it was explicitly "declared". Silly me.
 
>>  #############################
>>  # [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.

It might be ugly, but I think I can live with it. For now, the single custom error works. I was just trying to be critical towards my own code. And it was fun to write a factory class, of course. ;-)

> 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:

I tried doing that. Semantically (I mean just by the looks of the messages), I can categorize them. Then I end up with about 15 exceptions. After having done that, I started to be convinced not to follow this route. It might be nice to be able to choose whether warnings (retcodes < 0) should be treated as errors or not (I believe in options() of CRAN R this is called warningsAsErrors). Would the code below be a good implementation (I also experimented with cmp)?

# this is a global variable (perhaps in __init__.py)
# if warningsAsErrors > 0, even warnings will  raise an error.
import operator
warningsAsErrors = bool(os.getenv("warningsAsErrors"))
warningsAsErrors = operator.ne if warningsAsErrors else operator.gt
# this comes after each C function call
retcode = someCfunc(... blah...)
if retcode and oper(retcode, 0):
    raise MyCustomError


> 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
> 

DesertSpoonError? LOL! ;-)

> 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
> _______________________________________________
> Tutor maillist  -  Tutor at python.org
> To unsubscribe or change subscription options:
> http://mail.python.org/mailman/listinfo/tutor
> 


More information about the Tutor mailing list