Every context manager is like making a sandwich. It has three parts:
- The Top Bun (Setup): Getting the resource ready (Opening a file, connecting to a database).
- The Filling (The Logic): The code you actually want to run inside the
withblock. - The Bottom Bun (Teardown): Cleaning up (Closing the file, disconnecting) so you don’t leave a mess.
The with keyword ensures that no matter what happens to the filling, the Bottom Bun always gets put in place.
Class-Based Approach (__enter__ & __exit__)
This is the “formal” way to do it. You use two magic methods:
__enter__: Prepares the resource and returns it.__exit__: Handles the cleanup.
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
# Setup: Open the resource
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, traceback):
# Teardown: Close it automatically
self.file.close()
with FileManager("test.txt", "w") as f:
f.write("Using a custom class!")
Three arguments in __exit__ (exc_type, exc_val, traceback) are used to handle errors that happened inside the with block. If no error happened, they are all None.
Decorator Approach
Using contextlib is much more common because it’s shorter and uses a Generator. It feels like hitting “Pause.”
- Code before
yield= Setup (Top Bun). - The
yield= The Pause (Where yourwithcode runs). - Code after
yield= Cleanup (Bottom Bun).
from contextlib import contextmanager
@contextmanager
def simple_open(filename, mode):
f = open(filename, mode) # Setup
try:
yield f # Pause and let the user work
finally:
f.close() # Cleanup happens NO MATTER WHAT
Why the try/finally?
If the code inside your with block crashes, the code after yield might never run. By using finally, you guarantee the “Bottom Bun” (closing the file) happens even if the program explodes.
Real-World Example
Imagine you are working on a project and need to jump into a specific folder to check some files, but you want to be “teleported” back to your original folder automatically when you’re done.
import os
from contextlib import contextmanager
@contextmanager
def visit_folder(destination):
original_path = os.getcwd() # Remember where we started
os.chdir(destination) # Teleport to the new folder
try:
yield # Do your work here
finally:
os.chdir(original_path) # Teleport back home automatically
# Now we can jump in and out safely
print(f"I am at: {os.getcwd()}")
with visit_folder("Summer_Photos"):
print(f"I am now looking at photos in: {os.getcwd()}")
print(f"I am safely back at: {os.getcwd()}")