1

What is the better way to organize exception in a Python project? What is the right way to use exception description?

For example I have a function that parse email and return some data from it's attachment. It should raise an exception that specify and explain an error in a better way.

Options:

  1. Use one general exception for a process and it's message to specify the error:

    class ParseMailError(Exception):
        pass

    def get_parsed_mail(mail):
        if not mail.attachment:
            raise ParseMailError('No attachment')

        if not check_some_necessary_fields_format(mail):
            raise ParseMailError('Invalid attachment field format')

        ...
  1. For every case create an exception:

    class ParseMailError(Exception):
        pass

    class MailNoAttachmentError(ParseMailError):
        pass

    class MailInvalidFieldError(ParseMailError):
        pass

    def get_parsed_mail(mail):
        if not mail.attachment:
            raise MailNoAttachmentError

        if not check_some_necessary_fields_format(mail):
            raise MailInvalidFieldError

        ...

Is there any explanation of how to make it better?

2

Occam's razor:

More things should not be used than are necessary.

Python gives you the ability to subclass exceptions. That extensibility is great, when you need it. But it's rare you actually need to do that. Often you can get just as much error reporting clarity through customizing the message of a standard exception type. E.g.:

def get_parsed_mail(mail):
    if not mail.attachment:
        raise ValueError('No attachment')

    if not check_some_necessary_fields_format(mail):
        raise ValueError('Invalid attachment field format')

Standard exceptions belong to a well-structured, battle-proven hierarchy that many developers have studied and more-or-less understand.

Custom exceptions do help you signal the module that generated the error ("this error came from MailParser!") and as well as its kind, but often that information is overkill. Consider:

try:
    mail_parts = ParseMail.parse(raw_message)
except Exception as e:
    ...

Viewed in the context it will be used, the source isn't a mystery. You know from context that the error came from ParseMail. You know that whether it's a customized MailNoAttachmentError or merely a ValueError.

Other than source, different exception types can help you figure the kind of error. What happened, exactly? The real question is, does distinguishing those types help respond to error conditions? Do you expect there to be error handlers for each of these different types of error?

try:
    mail_parts = ParseMail.parse(raw_message)
except MailNoAttachmentError as e:
    ...
except MailInvalidFieldError as e:
    ...

Is there anything that a program can do to correct those individual cases? Or are you just reporting out to the developer / user? If just reporting to a human, customizing the exception's message is at least as important as the type of the exception. Probably more so.

So custom exceptions are only occasionally necessary. Piggybacking standard exceptions, but using your own customized messages, generally works just as well and is simpler.

But if you feel there is virtue in more precise error source/type signaling, that's fine too. A single kind of custom error, usually subclassed off a common error like ValueError, suffices for most modules, e.g.:

class ParseMailError(ValueError):
    pass

If you wanted to go full Java-esque exception diversity / precision, you can create your own ParseMail exception hierarchy blended with the standard hierarchy through multiple inheritance:

class ParseMailError(Exception):
    pass

class MailNoAttachmentError(ValueError, ParseMailError):
    pass

class MailInvalidFieldError(TypeError, ParseMailError):
    pass   
| improve this answer | |
7

A typical good practice is to have an 'umbrella' exception class per library / module / subject area, and have more specific exceptions as subclasses of it:

CarException  # The umbrella class
  NoFuelException
  FlatWheelException
  EngineException
    SparkPlugException  # You can get as specific as you want

Benefits:

  • Clients are sure they catch all exceptions your car library can raise if they catch CarException.
  • Clients are free to catch more specific exceptions if they wish, and obtain problem-specific details.
  • The library authors can add new exceptions, or make existing exceptions more detailed, without breaking existing client code.
| improve this answer | |
1

While this is generally a case-by-case situation, I like the suggestions given on the official C++ site: https://isocpp.org/wiki/faq/exceptions#mindset-for-proper-use-of-eh

Of course, those opinions are tailored towards C++ and not python, but many concepts are still relevant. Notably (emphasis mine):

Here are some “wrong exception-handling mindsets” in no apparent order:

...

Organizing the exception classes around the physical thrower rather than the logical reason for the throw: For example, in a banking app, suppose any of five subsystems might throw an exception when the customer has insufficient funds. The right approach is to throw an exception representing the reason for the throw, e.g., an “insufficient funds exception”; the wrong mindset is for each subsystem to throw a subsystem-specific exception.

| improve this answer | |
0

There is no one or "best" way to do this, its all depends on what you are looking to achieve.

from your examples, if the only difference is text, that stick with #1 if you got different logic to handle each case, than consider going with #2

between the two options, I think #2 is more cleaner, will make your functions more readable, much better reusability potential, and holds better design for future enhancements in the long term. But only if you plan doing other stuff than just passing different text messages, i will go this way.

while #1 is much more "local" solution and you will required more maintenance if change is required, or more functionality needs to be added.

You could start with #1 and move to #2.

hope that helped.

| improve this answer | |
0

Consider you have a task that may fail for a number of anticipated reasons. You would create and use one new exception type for that. You would wrap the (call to) the task in an exception handler that handles your type of exception.

That is the rule. Now it all depends on the granularity you find useful and/or convenient. What tasks do you recognize? What do you regard to be "one thing to do"?

The question may return on multiple levels in you application.

So it really depends on the tasks you care to recognize.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.