What Python 3.3 did to improve exception handling in your code

Explore exception handling and other underutilized but still useful Python features.
42 readers like this.
Coding on a computer

This is the fourth in a series of articles about features that first appeared in a version of Python 3.x. Python 3.3 was first released in 2012, and even though it has been out for a long time, many of the features it introduced are underused and pretty cool. Here are three of them.

yield from

The yield keyword made Python much more powerful. Predictably, everyone started using it to create a whole ecosystem of iterators. The itertools module and the more-itertools PyPI package are just two examples.

Sometimes, a new generator will want to use an existing generator. As a simple (if somewhat contrived) example, imagine you want to enumerate all pairs of natural numbers.

One way to do it is to generate all pairs in the order of sum of pair, first item of pair. Implementing this with yield from is natural.

The yield from <x> keyword is short for:

for item in x:
    yield item
import itertools

def pairs():
    for n in itertools.count():
        yield from ((i, n-i) for i in range(n+1))
list(itertools.islice(pairs(), 6))
    [(0, 0), (0, 1), (1, 0), (0, 2), (1, 1), (2, 0)]

Implicit namespace packages

Imagine a fictional company called Parasol that makes a bunch of stuff. Much of its internal software is written in Python. While Parasol has open sourced some of its code, some of it is too proprietary or specialized for open source.

The company uses an internal DevPI server to manage the internal packages. It does not make sense for every Python programmer at Parasol to find an unused name on PyPI, so all the internal packages are called parasol.<business division>.<project>. Observing best practices, the developers want the package names to reflect that naming system.

This is important! If the package parasol.accounting.numeric_tricks installs a top-level module called numeric_tricks, this means nobody who depends on this package will be able to use a PyPI package that is called numeric_tricks, no matter how nifty it is.

However, this leaves the developers with a dilemma: Which package owns the parasol/__init__.py file? The best solution, starting in Python 3.3, is to make parasol, and probably parasol.accounting, to be namespace packages, which don't have the __init__.py file.

Suppressing exception context

Sometimes, an exception in the middle of a recovery from an exception is a problem, and having the context to trace it is useful. However, sometimes it is not: the exception has been handled, and the new situation is a different error condition.

For example, imagine that after failing to look up a key in a dictionary, you want to fail with a ValueError() if it cannot be analyzed:

import time

def expensive_analysis(data):
    time.sleep(10)
    if data[0:1] == ">":
        return data[1:]
    return None

This function takes a long time, so when you use it, you want to cache the results:

cache = {}

def last_letter_analyzed(data):
    try:
        analyzed = cache[data]
    except KeyError:
        analyzed = expensive_analysis(data)
        if analyzed is None:
            raise ValueError("invalid data", data)
        cached[data] = analyzed
    return analyzed[-1]

Unfortunately, when there is a cache miss, the traceback looks ugly:

last_letter_analyzed("stuff")
    ---------------------------------------------------------------------------

    KeyError                                  Traceback (most recent call last)

    <ipython-input-16-a525ae35267b> in last_letter_analyzed(data)
          4     try:
    ----> 5         analyzed = cache[data]
          6     except KeyError:


    KeyError: 'stuff'

During handling of the above exception, another exception occurs:

    ValueError                                Traceback (most recent call last)

    <ipython-input-17-40dab921f9a9> in <module>
    ----> 1 last_letter_analyzed("stuff")
    

    <ipython-input-16-a525ae35267b> in last_letter_analyzed(data)
          7         analyzed = expensive_analysis(data)
          8         if analyzed is None:
    ----> 9             raise ValueError("invalid data", data)
         10         cached[data] = analyzed
         11     return analyzed[-1]


    ValueError: ('invalid data', 'stuff')

If you use raise ... from None, you can get much more readable tracebacks:

def last_letter_analyzed(data):
    try:
        analyzed = cache[data]
    except KeyError:
        analyzed = expensive_analysis(data)
        if analyzed is None:
            raise ValueError("invalid data", data) from None
        cached[data] = analyzed
    return analyzed[-1]
last_letter_analyzed("stuff")
    ---------------------------------------------------------------------------

    ValueError                                Traceback (most recent call last)

    <ipython-input-21-40dab921f9a9> in <module>
    ----> 1 last_letter_analyzed("stuff")
    

    <ipython-input-20-5691e33edfbc> in last_letter_analyzed(data)
          5         analyzed = expensive_analysis(data)
          6         if analyzed is None:
    ----> 7             raise ValueError("invalid data", data) from None
          8         cached[data] = analyzed
          9     return analyzed[-1]


    ValueError: ('invalid data', 'stuff')

Welcome to 2012

Although Python 3.3 was released almost a decade ago, many of its features are still cool—and underused. Add them to your toolkit if you haven't already.

What to read next
Tags
Moshe sitting down, head slightly to the side. His t-shirt has Guardians of the Galaxy silhoutes against a background of sound visualization bars.
Moshe has been involved in the Linux community since 1998, helping in Linux "installation parties". He has been programming Python since 1999, and has contributed to the core Python interpreter. Moshe has been a DevOps/SRE since before those terms existed, caring deeply about software reliability, build reproducibility and other such things.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.