Stop Using Exceptions Like This in Python

Imagine you are building a blogging app. In the normal flow of your blogging app, users are able to sign up and create blog posts.

An exception (error) is said to occur when the normal flow of your app is interrupted. For example, when a user signs up with an existing email on your blog site.

Exception handling means deciding what to do with this interruption as app developers. In this scenario, we could return an error message back to our users, write an error to our logging system, etc.

Error handling is essential in building applications, it makes our code more maintainable.

To raise an exception in Python, we could do the following:

raise ValueError("Something has gone wrong.")

In this article, I intend to share some Python error-handling tips and some common bad practices to avoid.

TL;DR

  • Never use bare except
  • Avoid catching or raising generic Exception
  • Refrain from just passing in except blocks.

1. Avoid Bare except

The first rule of thumb is to avoid using bare except, as it doesn't give us any exceptions object to inspect.

Furthermore, using bare except also catches all exceptions, including exceptions that we generally don’t want, such as SystemExit or KeyboardInterrupt.

Here’s an example of using bare except, which is not recommended:

# Do NOT ever use bare exception:

try:
    return fetch_all_books()

except:  # !!!
    raise

Never ever use bare except.

So, what’s wrong with catching every exception?

Catching every exception could cause our application to fail without us knowing why. This is a horrible idea when it comes to debugging.

Here’s an example: We’re building a feature that allows users to upload a PDF file. Imagine we put a generic try-catch around that block of code.

Below, we catch a generic exception saying the file is not found no matter what the actual problem is:

def upload_proof_of_address_doc(file):
    try:
        result = upload_pdf_to_s3(file)
        if not result:
            raise Exception("File not found!")

    except:
        raise

Example of why we should not raise or catch a generic Exception.

Regardless of the actual problem (i.e. endpoint is read-only, no permissions, or invalid file type), whoever has a problem uploading the file would just get back an error claiming the file was not found.

Now imagine the file is uploaded, but the user uploaded an image file instead. The user keeps getting back the “file not found” error as they retry and retry. Eventually, they start screaming, “The file is right there! This app is crap!”

Instead, the right way to handle this is to introduce specific exception classes to handle all three scenarios mentioned.

2. Stop Using raise Exception

Secondly, we should avoid raising a generic Exception in Python because it tends to hide bugs.

Here's another example that you should avoid using:

# Do NOT raise a generic Exception:

def get_book_List():
    try:
        if not fetch_books():
            raise Exception("This exception will not be caught by specific catch")  # !!!

    except ValueError as e:
        print("This doesn't catch Exception")
        raise


get_book_List()
# Exception: general exceptions not caught by specific handling

Avoid this error-hiding anti-pattern!

While there are plenty of ways to write bad code, this is one of the worst anti-patterns (known as error hiding).

In my personal experience, this pattern has stood out as being the greatest drain on developer productivity.

3. Stop Using except Exception

As developers, we tend to wrap our function code with a try-except block on autopilot mode. We love doing this because we know that there is always a chance of exceptions being thrown.

Can’t ever be too safe, right?

# Do NOT catch with a generic Exception:

def fetch_all_books():
    try:
        if not fetch_all_books():
            raise DoesNotExistError("No books found!")

    except Exception as e:  # !!!
        print(e)
        raise

No! Stop using except Exception!

However, the caveat is that developers tend to catch exceptions with a generic BaseException or Exception class.

In this scenario, it means that we will catch everything. Everything including exceptions that we cannot or should not recover from.

Plan, plan, and plan

Rather, we should always plan ahead and figure out what can break and what exceptions are expected to be thrown.

For instance, if we’re working with a database call to fetch a user profile with email, we should expect that the email might not exist and handle it.

In this scenario, we can raise a custom UserDoesNotExist error and prompt our users to try again, allowing our app to recover from the exception.

class UserDoesNotExist(Exception):
    """Raised when user does not exist"""
    pass

Before writing our own custom user-defined exceptions, we should always check if the library that we are using has its own built-in exceptions that meet our use case.

In short, we should only catch errors that we are interested in with a specific Exception that semantically fits our use case.

Finally, here’s a better example of how to handle an exception:

# Do:

def fetch_user_profile(id):
    try:
        user = get_user_profile(id)
        if not user:
            raise UserDoesNotExist("User does not exist.")  # Raise specific exception

    except UserDoesNotExist as e:  # Catch specific exception
        logger.exception(e)  # Logs the exception
        raise  # Just raise
        # raise UserDoesNotExist # Don't do this or you'll lose the stack trace

We could add more specific exceptions to handle different exceptions.

But, I don’t know what exceptions to use

Understandably, it’s very unlikely that we are always prepared for every possible exception.

In such cases, some people suggest that we should at least catch them with Exception, as it won't include things like GeneratorExist, SystemExit, and KeyboardInterrupt, which will terminate our app.

I’d argue that we should spend the time to figure out what the exceptions are. When catching generic exceptions becomes a habit, it becomes a slippery slope.

Here’s another good example of why we should not catch generic exceptions.

4. Refrain From Passing in except Blocks

When designing an app, there might be specific exceptions where we are completely fine without doing anything.

However, the worst possible thing a developer can do is the following:

# Do NOT EVER pass a bare exception:
try:
    compute_combination_sum(value)

except:
    pass


# Do NOT do this:
try:
    compute_combination_sum(value)

except BaseException:
    pass

A bad example that we should not use

The code above implies that despite the fact that we are not ready for any exceptions, we are catching any exceptions willingly.

Another disadvantage of passing and catching Exception (or bare except) is that we will never know the second error when there are two errors in the code. The first error will always be caught first and we will get out of the try block.

If we’re just passing an except statement, it’s a good sign that we aren’t really prepared for the exception that we are catching. Perhaps it’s a good time to rethink and refactor.

Log, don’t pass

Nevertheless, if we don’t have to do anything about the exception, we should at least use a more specific exception while also logging the exception.

import logging

logger = logging.getLogger(__name__)


try:
    parse_number(value)

except ValueError as e:
    logger.exception(e)

Log, don’t pass!

Besides considering including some recovery code, we can also add a comment to inform other developers about the use case.

The bottom line is, we should steer away from passing in except blocks unless explicitly desired. Again, this is usually a bad sign.

Ultimately, we should log the exception to a monitoring system so that we at least have a log of what actually went wrong.

Closing Thoughts

To summarize everything we went through in this article, we should:

  • Never use bare except.
  • Stop raising generic Exception.
  • Stop catching generic Exception.
  • Refrain from passing in except blocks.

In most situations, it’s often better for the app to fail at the point of an exception rather than having our app continue to behave in weird unexpected ways. Hence, it’s best to catch only the exceptions that we know and intend to handle.

That’s all! Happy coding!

23