Python Context Managers: enter and exit

Context managers in Python provide an efficient mechanism for resource management, ensuring that resources like files, database connections, and locks are properly acquired and released. This automation is powered by the with statement, which relies on the context management protocol defined by two runtime lifecycle methods: __enter__ and __exit__. This article explains the responsibilities of each method, details their execution flow, demonstrates how to handle exceptions within them, and provides a practical implementation of a custom context manager.

The Role of __enter__

The __enter__ method prepares the execution context. When a with block begins, the runtime first instantiates the context manager object and immediately calls its __enter__ method.

class ManagedResource:
    def __enter__(self):
        print("Acquiring resource")
        return self

The Role of __exit__

The __exit__ method guarantees teardown logic, executing regardless of whether the code inside the with block finishes successfully or raises an unhandled error.

The __exit__ method requires four arguments: self, and three exception-related parameters:

  1. exc_type: The exception class (e.g., ValueError), or None if no error occurred.
  2. exc_val: The exception instance or message, or None.
  3. exc_tb: The traceback object, or None.
class ManagedResource:
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Releasing resource")
        # Returning True suppresses any raised exception
        return False

Exception Handling in __exit__

The return value of __exit__ dictates how errors are handled:

Execution Flow Under the Hood

When Python encounters a context manager:

with ContextManager() as resource:
    # Operations using resource

The interpreter translates that code to behave equivalently to the following pattern:

manager = ContextManager()
resource = manager.__enter__()
exception_occurred = True

try:
    # Operations using resource
    exception_occurred = False
except Exception:
    if not manager.__exit__(*sys.exc_info()):
        raise
finally:
    if not exception_occurred:
        manager.__exit__(None, None, None)

Practical Implementation

The following example demonstrates a custom file-handling context manager that opens a file, yields access to it, and ensures it closes correctly even if an error is raised.

class CustomFileHandler:
    def __init__(self, filepath, mode):
        self.filepath = filepath
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filepath, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        if exc_type is not None:
            print(f"Handled error: {exc_val}")
            return True  # Suppresses the exception

Using this implementation:

with CustomFileHandler("example.txt", "w") as f:
    f.write("Writing data securely.")
    raise RuntimeError("Something went wrong during write.")

# Execution continues here because __exit__ returned True
print("Execution continues past the with block.")

By encapsulating allocation in __enter__ and release logic in __exit__, Python context managers eliminate resource leaks and boilerplate try...finally structures across applications.