Last Updated on 2024-09-12 by Clay
In Python, the context manager decorator @contextmanager
from the contextlib
module allows developers to conveniently create our own context manager.
What is a context manager? Simply put, it defines a block that handles certain logic, and it automatically executes some code when 'entering' and 'exiting' that block. In Python, the most common usage can be seen with the with
syntax, which automatically handles operations like opening/closing files or allocating/releasing resources.
Context Manager Without @contextmanager
If we want to create a context manager for managing files, the traditional approach would be to define a class and then define __enter__()
and __exit__()
methods to control the logic when entering and exiting.
from typing import Any
class FileManager:
def __init__(self, filename: str, mode: str) -> None:
self.file = None
self.filename = filename
self.mode = mode
def __enter__(self) -> Any:
print("Open file.")
self.file = open(file=self.filename, mode=self.mode)
return self.file
def __exit__(self, exc_type, exc_value, traceback) -> None:
print("Close file.")
if self.file:
self.file.close()
if __name__ == "__main__":
with FileManager("test.log", "w") as f:
f.write("This is a test log.")
Output:
Open file.
Close file.
Of course, we can check if the file was successfully written. In short, our defined FileManager
class will open the file and return the file object self.file
when entering the with
block, and automatically close the file when exiting the block.
This is an effective and traditional way to do it, but let's now look at how @contextmanager
simplifies this process.
Using @contextmanager
@contextmanager
is meant to simplify the method we just defined.
from contextlib import contextmanager
@contextmanager
def open_file(filename: str, mode: str):
print("Open file.")
f = open(file=filename, mode=mode)
try:
yield f
finally:
print("Close file.")
f.close
if __name__ == "__main__":
with open_file("test.log", "r") as f:
print(f.read())
Output:
Open file.
This is a test log.
Close file.
The code does exactly the same thing as the previous class-based approach, but with a simplified syntax. In short, @contextmanager
essentially creates a generator. This generator pauses when it reaches the yield
statement, returning the yield
value to the variable following the with
statement (in our example, this is f
). Upon exiting, the context manager will always execute the code after the yield
, which makes it perfect for try...finally
blocks.
Here are a few scenarios where context managers are particularly useful:
- Releasing resources: In processes where resources must be released after completion, context managers are perfect for automatic exit handling.
- Temporarily changing variable values: Suppose you have a scenario where a variable needs to be temporarily changed during an operation, and then restored afterward. This can be handled using a context manager. (A recent use case I encountered was when updating a database — during the update, my system needed to be in maintenance mode, but even if there were errors in the user's data, the system had to return to online status afterward.)
- Accessing and releasing locks
To summarize, the usage of @contextmanager
can be understood as follows:
@contextmanager
is used to create simplified context managers, defining what to do when entering and exiting the context usingyield
.- When entering the context, the code before the
yield
is executed; upon exiting, the code after theyield
is executed, usually to clean up. - Using it with
try
andfinally
ensures that resources are released properly, even in case of exceptions.
References
- Python Docs - contextlib — Utilities for with-statement contexts
- Context Managers and Python's with Statement