Intro

Before we start, I have to tell you that you should “never suppress exceptions silently”. As Zen of Python states:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

It is a common case to perform an operation that is expected to fail sometimes, but should not interrupt the program, like throwing 500 internal server errors in a web application.

We have to handle these fails, errors carefully.

Let’s use a simple and dummy code snippet to explain.

def f(lst):
    """Dummy function to throw an error"""
    result = lst[10]
    print(f"{result = }")

    return result


if __name__ == "__main__":
    f(lst=list(range(10)))

If you run above code, you will get IndexError with a traceback like below:

Traceback (most recent call last):
  File "/tmp/show/show.py", line 21, in <module>
    f(lst=list(range(10)))
  File "/tmp/show/show.py", line 14, in f
    result = lst[10]
IndexError: list index out of range

In order to handle exceptions -like IndexError in our example, we usually use try, except, else, finally.

import logging

logging.basicConfig(
    level=logging.INFO,
    format="[%(asctime)s] [%(levelname)-7s] : %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S %z",
)

logger = logging.getLogger(__name__)


def g(lst):
    """Dummy function to show try/except"""
    try:
        result = lst[10]
    except IndexError:
        result = 0
        logger.warning("<lst> doesn't have 10 elements")
    else:
        logger.info("<lst> does have 10 elements")
    finally:
        logger.info("I always run")

    logger.info(f"{result = }")

    return result


if __name__ == "__main__":
    g(lst=list(range(10)))

Now if you run the code, you will get:

[2022-08-13 17:54:12 +0300] [WARNING] : <lst> doesn't have 10 elements
[2022-08-13 17:54:12 +0300] [INFO   ] : I always run
[2022-08-13 17:54:12 +0300] [INFO   ] : result = 0

This is how we handle errors in Python in a basic way.

In order to suppress IndexError you just simply pass the except block:

def x(lst, initial_value=0):
    """Dummy function to show basic suppress"""
    try:
        initial_value += lst[10]
    except IndexError:
        pass

    return initial_value


if __name__ == "__main__":
    logger.info(x(lst=list(range(10))))

Note

Never use Exception or BaseException for wildcard exception catching. You should always use specific exception that you expect to be raised.

# Don't use wildcard Exception catching

try:
    result = lst[10]
except Exception:
    result = 0

contextlib.suppress

I’d like to introduce that there is also another way to deal with exceptions; with a contextmanager named suppress.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def z():
    lst = list(range(10))
    result = 0
    with contextlib.suppress(IndexError):
        result = lst[10]

    logger.info(result)

    return result

if __name__ == "__main__":
    z()

This suppress is equivalent to:

try:
    result = list[10]
except IndexError:
    pass

Note

You can get more information from official documentation contextlib.suppress.

You may think where should or can I use suppress. To give you an instance pytest uses raises in a similar way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# some_test.py

import pytest

from show import f


def test_list_has_less_than_10_elements():
    lst = list(range(10))

    with pytest.raises(IndexError):
        assert f(lst) != 10


def test_list_has_more_than_10_elements():
    lst = list(range(11))

    assert f(lst) == 10

You may also just simply suppress an error rather than constructing a control flow logic.

Undoubtedly using contextlib.suppressdepends on business logic, code readability and maintainability. However I believe that it’s always good to know there are another alternatives to approach a problem.

All done!