Context Managers
- When opening files or acquiring locks, resources must be released even if errors occur.
- Manual
try...finally ensures cleanup but adds boilerplate and potential for mistakes.
- Forgetting to initialize the resource variable or to call cleanup in every exit path leads to leaks, deadlocks, or corrupted data.
- Cleaner patterns reduce noise and risk in automation scripts.
f = None
try:
f = open("my_log.txt", "w")
f.write("First line\n")
# Simulate an error
result = 1 / 0
f.write("Second line\n")
except:
print("Error has occurred.")
finally:
if f:
print("Closing file.")
f.close()
print(f"File closed: {f.closed}")
The with Statement Simplifies Cleanup
- The
with statement handles setup and teardown automatically for context managers.
- For file I/O,
with open(...) as f: guarantees f.close() on block exit, even if an exception is raised.
- Syntax is concise and idiomatic, reducing boilerplate and improving readability.
Common Context Manager Examples
- Files:
with open(...) as f: for automatic file closing.
- Locks:
with threading.Lock(): acquires and releases locks safely.
- Tempfiles/Dirs:
with tempfile.TemporaryDirectory() as d: creates and cleans up temporary directories.
- Context managers from the standard library cover most resource-management needs.
f = None
try:
with open("my_log.txt", "w") as f:
f.write("First line\n")
# Simulate an error
result = 1 / 0
f.write("Second line\n")
except:
print("Error has occurred.")
print(f"File closed: {f.closed}")
import tempfile, os
dir_name = None
with tempfile.TemporaryDirectory() as tempdir:
print(f"Created temp dir: {tempdir}")
dir_name = tempdir
test_file = os.path.join(tempdir, "test.txt")
with open(test_file, "w") as file:
file.write("Hello from temp directory.")
print(f"Files inside temp dir: {os.listdir(tempdir)}")
try:
contents = os.listdir(dir_name)
print(f"Contents of {dir_name}: {contents}")
except FileNotFoundError as e:
print(f"Expected error accessing removed directory: {e}")
Custom Resource Management: Writing Context Managers
- Whenever you need custom setup/teardown logic, you can write your own Context Manager.
- A context manager ensures that teardown always runs, even if errors occur in the block.
- Two approaches: implement
__enter__/__exit__ in a class or use the simpler generator-based decorator.
class MyContextManager:
def __init__(self, timeout):
self.timeout = timeout
def __enter__(self):
print("Setup complete")
return "a simple value"
def __exit__(self, exception_type, exception_value, traceback):
print(f"Teardown")
# Commenting out since we replaced *args for explicit
# exception_type, exception_value, traceback parameters
# for arg in args:
# print(arg)
return False
with MyContextManager(timeout=30) as cm:
print(cm)
print("Inside the block")
raise ValueError("Simulated problem")
The @contextlib.contextmanager Decorator
- Provided by the
contextlib module to turn a generator into a context manager.
- Decorated function needs exactly one
yield.
- Code before
yield runs as __enter__; code after (or in finally) runs as __exit__.
- Simplifies many common patterns without writing a full class.
Generator Structure for @contextmanager
- Wrap the
yield in try...finally to ensure teardown even on errors.
- The value yielded is bound to
as var in the with statement (if used).
- You can catch exceptions inside the generator if you want to suppress them.
import os
from contextlib import contextmanager
@contextmanager
def change_directory(destination):
"""
Temporarily switch into destination. If the directory does not exist,
it is created just before the switch.
Args:
destination (str): Path to the directory that should become the working directory
"""
origin_dir = os.getcwd()
try:
print(f"Changing into {destination}")
os.makedirs(destination, exist_ok=True)
os.chdir(destination)
yield os.getcwd()
finally:
print(f"Reverting to original dir: {origin_dir}")
os.chdir(origin_dir)
print(f"Start: {os.getcwd()}")
with change_directory("temp_dir") as new_dir:
print(f"Inside: {new_dir}")
print(f"End: {os.getcwd()}")