Adam Johnson

Home | Blog | Training | Projects | Colophon | Contact

Entering a Flaky Context Manager in Python

2020-02-07 Flaky Snowflakes

Here’s a little Python problem I encountered recently.

I want to open and read a file cautiously. If open() raises an OSError, for whatever reason, I want to do nothing. Otherwise, I want to print out all the lines in the file. If I open the file, I want to always close() it - even if the printing of the lines raises an error. But if the printing does raise an error, even an OSError, that should be raised.

What’s the most idiomatic way to do this?

If you have experienced similar problems before, you were likely thinking of using try around with open() like so:

try:
    with open("file.txt") as fp:
        for line in fp:
            print(line)
except OSError:
    pass

This works great, apart from for the last point: “if the printing does raise an error, even an OSError, that should be raised.” Right now such an OSError would be caught by the except OSError.

Following my post on limiting try clauses, we want to limit the try to only be around open(). Effectively, we want something like this non-existent, poorly indented syntax:

try:
    with open("foobar") as fp:
except OSError:
    pass
else:
        for line in fp:
            print(line)

It would be neat if it works, but it doesn’t. So how can we wrap a try around just our open context manager?

Enter the standard library’s contextlib.ExitStack. It’s a meta context manager, allowing you to enter zero or more context managers, and it will close them later for you.

To solve our problem, we need to want to use with ExitStack() combined with enter_context on our open() call. We then use a standard try/except/else to catch OSError on the open() but nowhere else. The full solution looks like:

from contextlib import ExitStack


with ExitStack() as stack:
    try:
        fp = stack.enter_context(open("file.txt", "r"))
    except OSError:
        pass
    else:
        for line in fp:
            print(line)

Update (2020-02-07): As pointed out by Rawing7 on Reddit, open() is a bit special as a context manager. It opens the file when constructed, and its __enter__() only returns itself, so you don't really need ExitStack() to solve this particular problem. However, the technique is still useful for most other context managers, that actually do something in their __enter__() methods.

Fin

Thanks to Tom Grainger for reminding me to use ExitStack() here. I hope this helps you use context managers betterer,

Adam


Working on a Django project? Check out my book Speed Up Your Django Tests which covers loads of best practices so you can write faster, more accurate tests.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: python