Every context manager is like making a sandwich. It has three parts:

  1. The Top Bun (Setup): Getting the resource ready (Opening a file, connecting to a database).
  2. The Filling (The Logic): The code you actually want to run inside the with block.
  3. 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.”

  1. Code before yield = Setup (Top Bun).
  2. The yield = The Pause (Where your with code runs).
  3. 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()}")