Skip to content

A Comprehensive Python Guide to Context Managers and the 'with' Statement

Updated: at 04:23 AM

Context managers in Python provide an elegant and Pythonic way to manage resources within a code block. The with statement works together with context managers to ensure proper acquisition and release of resources. This prevents issues like resource leaks or unwanted side effects from improperly managed resources. In this guide, we will take a deep dive into context managers in Python and how to use them effectively.

Table of Contents

Open Table of Contents

What is a Context Manager?

A context manager is an object that implements the context management protocol by defining the __enter__() and __exit__() dunder methods. Together these methods allow acquiring and releasing of resources precisely when needed.

The key advantage of using a context manager is that it guarantees the __exit__() method will execute after the with block finishes, even if exceptions occur within the block. This makes them ideal for tasks like:

Let’s explore a simple example of a context manager that prints when it is entered and exited:

class SimpleContextManager:
    def __enter__(self):
        print('Entering context')

    def __exit__(self, exc_type, exc_value, exc_traceback):
        print('Exiting context')

When used with a with statement, this context manager will print out messages before and after the with block:

with SimpleContextManager() as scm:
    print('Within context')

# Output:
# Entering context
# Within context
# Exiting context

The __enter__() method is called at the start of the with block, allowing us to set up the context. The __exit__() method then releases the context after the block finishes.

The with Statement

The with statement works hand-in-hand with context managers in Python. The general syntax is:

with EXPRESSION as TARGET:
    BLOCK

Let’s break this down step-by-step:

  1. The EXPRESSION is evaluated to get a context manager object
  2. The context manager’s __enter__() method is called, with the return value bound to TARGET
  3. The code in BLOCK executes
  4. If BLOCK finishes executing, the context manager’s __exit__() method is called to clean up resources
  5. If an exception is raised in BLOCK, __exit__() is still called to do cleanup before the exception is re-raised

So the with statement provides a clean, Pythonic way to set up and tear down resources. We get the acquisition and release logic automated for us through the context manager protocol.

Real World Example: File I/O

One of the most common uses of context managers is for file input/output (I/O) operations. By using a context manager, we can ensure the file is properly closed after being read from or written to.

Let’s see an example without a context manager:

file = open('data.txt')
data = file.read()
# Process data
file.close()

This works, but if an exception occurs while processing the data, we would neglect to close the file handle. Over time this could lead to resource leaks.

We can improve this with a context manager using the with statement:

with open('data.txt') as file:
    data = file.read()
    # Process data

# File is automatically closed

Now the file is guaranteed to close, even if exceptions occur. The open() function acts as a context manager in Python, so we get this behavior for free!

Defining Your Own Context Managers

Sometimes we want to define our own context managers for specific use cases. As we saw earlier, any object that implements __enter__() and __exit__() can serve as a context manager.

Here is an example custom context manager that temporarily changes the working directory:

import os
from contextlib import contextmanager

@contextmanager
def change_dir(destination):
    """Context manager to temporarily change directories"""

    # Store original directory
    original_dir = os.getcwd()

    try:
        # Change to destination directory
        os.chdir(destination)
        yield

    finally:
        # Change back to original directory
        os.chdir(original_dir)

We use the @contextmanager decorator from the contextlib module to turn our generator function into a context manager. Inside the function:

  1. The original working directory is saved
  2. We change to the destination directory and yield control back to the caller
  3. After the with block, the finally clause restores the original working directory

Now we can use this custom context manager as follows:

with change_dir('data'):
    print(os.listdir())
    # Work with files in data dir

# Original working directory is restored

The context manager encapsulates the logic to temporarily change directories, then change back after the with block.

Using contextlib Utilities

The contextlib module provides helpful utilities for working with context managers in Python:

@contextmanager

As we saw earlier, this decorator converts a generator function into a context manager. We yield control back to the caller from __enter__() and cleanup in finally.

@asynccontextmanager

Similar to @contextmanager but for creating asynchronous context managers usable in async/await code.

@closing

Wraps an object’s close() in a context manager __exit__() method so it is called after the with statement. Helpful for objects with close methods but no built-in context behavior.

import contextlib

@contextlib.closing
def get_connection():
  return psycopg2.connect(connection_string)

with closing(get_connection()) as conn:
  # Use connection

nullcontext(enter_result=None)

Returns a context manager that does nothing on enter/exit but yields enter_result. Useful as a no-op placeholder.

suppress(*exceptions)

Context manager that suppresses specified exceptions if they occur in the with block.

redirect_stdout(new_target)

Temporarily redirects sys.stdout to another file or file-like object.

ContextDecorator

A base class to simplify creating context managers that can also work as decorators.

There are many more contextlib utilities for scenarios like dynamically exiting context managers and stacking them. Consult the documentation for reference.

Best Practices for Using Context Managers

Here are some tips for effectively using context managers in Python:

Conclusion

Context managers in Python provide an elegant approach to managing resources and state within a code block. By using the with statement together with objects that implement the context management protocol, we can streamline code and reduce bugs caused by improper cleanup.

The contextlib module supplements context managers with useful decorators and utilities. We can leverage these tools to create custom context managers suited for specific tasks. Context managers are a prime example of Python’s focus on developer productivity and cleaner code. By mastering their use, we can write more Pythonic applications that safely manage resources.