Python : Context Managers Are Not Just for Opening Files

Context managers allow Python to guarantee that a resource will be cleaned up, whether the block’s code succeeds or fails. A technical article details how they work under the with statement, as well as several ways to build and use them, far beyond just file handling.

Many developers discover this mechanism with with open(path), which automatically closes the file. According to the author, files are actually the least interesting case: the with statement primarily delimits a block whose exit is guaranteed. The protocol relies on two methods, __enter__(), called upon entering the block, and __exit__(), called upon exiting, whether normally, caused by a return, or due to an exception.

The value returned by __exit__() plays a decisive role. If it is truthy, the exception is suppressed, which can go unnoticed: the program continues, and the error disappears without a trace. If it is False or absent, the exception propagates after cleanup. This is why the managers presented in the article never return a truthy value.

Building your own managers

A class is suitable when the manager needs to maintain state that the caller can read back. The article gives the example of a timer, which measures a block’s duration using time.perf_counter() and displays the result even if the block fails. An interrupted feature extraction thus displays “feature extraction: 12.418s (failed)”, before the error propagates.

For a linear task, i.e., a setup, a block, then a teardown, the @contextmanager decorator allows writing a simple generator function. Everything before the yield executes upon entry, everything after upon exit. The article recommends placing the teardown in a finally block, and never swallowing the exception in a try/except without re-raising it. Only one value should be yielded, otherwise a RuntimeError is raised.

Restoring state and managing multiple resources

The standard library already provides several managers. TemporaryDirectory() creates a uniquely named directory and deletes it at the end of the block, avoiding leftover files from a previous run. Since Python 3.12, the delete=False parameter allows keeping the files for later analysis. On Windows, a file left open can cause deletion to fail; the ignore_cleanup_errors=True parameter, available since Python 3.10, mitigates this issue without solving it.

For dictionary modifications, patch.dict(), from the unittest.mock module, restores the original state after the block: an existing key regains its value, and an absent key is removed. The article notes, however, that os.environ is global to the process. All tasks and threads then see the modification, making it risky in concurrent code.

When the number of resources is only known at runtime, for example the number of files to merge, ExitStack groups managers. Its enter_context() method registers a manager, and callback() registers a simple function. Upon exit, the items are popped in reverse order. The article specifies that this popping occurs at the end of the block or upon a call to close(), not when the object is garbage-collected. A dedicated class may be preferable when the stack becomes too heavy.