Python context manager and the 'with' statement
Our code often acquires resources (files, network connections, locks, etc.) that must be released when done. Way too often, we forget to release such resources, causing hard-to-find bugs. Even experienced developers can face unexpected interruptions that prevent proper cleanup. Python runtime contexts (or simply contexts) offer a convenient way to handle these issues.
TLDR
Python context managers and the
with statement provide automatic resource allocation and cleanup. A context is an object implementing __enter__ and __exit__ methods for allocation and cleanup, respectively. Upon entering the context, __enter__ is called while exiting triggers a call to __exit__, even in case of exceptions. You can implement these methods directly, or use the contextlib module, which provides abstract classes and decorators to reduce boilerplate.Coding without context
The most natural way to open a file in Python is the usual open-use-close sequence of operations.
f = open("test.txt", "w")
f.write("Hello")
f.close()
With this code, we may forget to close the file, which could lead to errors down the line. For example, what will this code write to
/tmp/aa.txt?f = open("/tmp/aa.txt", "w")
f.write("Hello")
f2 = open("/tmp/aa.txt", "w")
f2.write("Hell")
f.close()
f2.close()
In all my experiments, the file contains the string
Hello. The write on f2 does not have an effect, simply closing f before opening f2 produces the expected output. You can easily imagine this code spread through several functions and/or files, making it a nightmare to debug (what the Hello is happening here?).Using context, we enter the context using the
with statement and use the resource.with open("test.txt", "r") as f:
# Entered the context
print(f.read())
# Exited the context
The context takes care of releasing after leaving the context (exiting the
with block). It is still possible to create buggy code as the one above by opening f2 within the context, but it becomes clearer that f is open within this context.Warning
Our code isn’t truly concurrent, there are no multiple threads accessing the same resources. Nonetheless, we run into problems because the actual writing happens only on close rather than on the actual call to
write.Context manager
Entering and exiting contexts is managed by context managers — objects that implement
__enter__ and __exit__ methods. It is also possible to use the contextmanager decorator from contextlib module if you don’t need a custom cleanup implementation.As an example, let's create a context manager that turns console printing red. We use ANSI codes to enable red coloring
\33[31;10m and to reset to default \33[0m.class PrintRed:
def __enter__(self):
print("\033[31;10m", end="")
return self # don't forget this!
def __exit__(self, exc_type, exc_value, traceback):
print("\033[0m", end="")
return False
print("Before context")
with PrintRed() as c:
print("Within context...")
print("...more in context")
print("After context")
This simple example illustrates the basic syntax and usage of context managers. First, we define an object
PrintRed that has __enter__ and __exit__ methods, they implement resource allocation and cleanup, respectively. To use a context, we only need to create it in a with statement, the block that comes after can then use the variable c, which refers to the created instance.Note
In the above example, the variable
c is never used, this is a common case and the with statement works even if we remove the as part. with PrintRed():
# block goes here
Outside the block, for the
with statement, the object c is not valid and, more importantly, the __exit__ method has been called.Although the above examples are quite simple, the built-in
contextlib module allows us to define contexts even more elegantly.Contextlib module
The
contextlib built-in module “provides utilities for common tasks involving the with statement” [1].The AbstractContextManager class
The first utility we discuss from
contextlib is the abstract class AbstractContextManager, which defines a simple __enter__ method returning self and leaves the implementation of __exit__ abstract for the programmer to define it.from contextlib import AbstractContextManager
import sqlite3
class OpenDB(AbstractContextManager):
def __init__(self, db_path=".db.sqlite"):
self.db_path = db_path
self.db = sqlite3.connect(self.db_path)
def add(self, content="Something to add"):
print(f"{content}\n will be added to the DB")
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
return False
with OpenDB() as db:
db.add()
The logic to open a connection to the database is implemented in the
__init__ method. In this case, the __enter__ method would simply return self, exactly what provided by AbstractContextManager implementation if __enter__.Async support
The
contextlib package supports asynchronous methods __aenter__ and __aexit__ through the class AbstractAsyncContextManager.Besides abstract classes,
contextlib provides several utilities to deal with contexts in Python, the reference is the place to look for more on this. Next, we are going to understand what are and what can be used for the parameters in the __exit__ method.Parameters of __exit__
Python contexts ensure resource cleanup, even if something goes wrong. Even if exceptions raise, we want cleanup code to be executed. To guarantee that, context managers invoke the
__exit__ function even if the block in with is exited due to an exception.When we arrive at
__exit__ by exception managing, the three parameters of __exit__ contain all the useful information about the exception.exc_typecontains the type of exception, which is the class ofexc_value.exc_valuecontains the exception object, the one that you would normally catch.tracebackcontains the traceback object associate with the exception (rarely used).
Using these parameters, you can manage exceptions if appropriate or simply decide to propagate the exception after the cleanup. To propagate the exception, simply return
False from __exit__ to block (swallow) the exception return True.from contextlib import AbstractContextManager
class IgnoreZeroDiv(AbstractContextManager):
def __exit__(self, exc_type, exc_value, traceback):
if exc_type.__name__ == 'ZeroDivisionError':
line = traceback.tb_lineno
print(f"Let's pretend you didn't divide by zero in line {line}")
return True
return False
with IgnoreZeroDiv():
x = 1/0
print(f"We won't arrive here: {x}")
Conclusion
Context managers are one of Python's most elegant features for handling resources safely and cleanly. Whether implemented manually or through
contextlib, they help reduce errors and make code more readable.References
- [1]:
contextlib— Utilities for with-statement contexts (Python documentation) - [2]: PEP 343 — The “with” Statement
🐻 📆 Bear of the day: Bagigi (Gigi)
