Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 51

Creating Context Managers with `@contextlib.contextmanager`

Recommended resources

Introduction

Welcome to the next lesson in our journey to flatten complex code structures. In our previous lesson, you mastered the use of the with statement, leveraging built-in context managers to ensure safe and automatic resource cleanup. This is a crucial step in writing robust Python code.

Today, we move from being a consumer of context managers to a creator. Your learning outcome is to write a custom context manager using the @contextlib.contextmanager decorator and a generator.

While you could build context managers using a full class with __enter__ and __exit__ methods—a pattern that might feel familiar from Java's AutoCloseable interface—Python provides a more concise and idiomatic tool for this job. You will learn to use a simple generator function combined with a decorator to achieve the same result with less boilerplate, making your own objects seamlessly integrate with the with statement.


1. Two Paths to a Context Manager: Class vs. Function

To appreciate the elegance of the decorator-based approach, it's useful to first see the more explicit, class-based method. This provides the "bad vs. good" comparison you appreciate and clarifies what's happening under the hood.

A class becomes a context manager by implementing two special "dunder" methods:

  • __enter__(self): The setup logic. Runs when the with block is entered. Its return value is bound to the as variable.
  • __exit__(self, exc_type, exc_value, exc_tb): The teardown logic. Runs when the with block is exited, guaranteed. It receives exception details if one occurred.

Python Tutorial: Context Managers - Efficiently Managing Resources

This video from Corey Schafer starts by demonstrating how to build a context manager using a class. This will give you a solid foundation for understanding what the more Pythonic approach simplifies.

Watch the segment from 01:35 to 08:20. Focus on how the __init__, __enter__, and __exit__ methods map to the setup, execution, and teardown phases of managing a file resource.

As you can see, the class-based approach is perfectly functional but involves a fair amount of ceremony. For most custom context managers, there is a much cleaner way.


2. The Pythonic Way: @contextlib.contextmanager

The contextlib module is part of Python's "batteries-included" standard library, and it contains the powerful @contextmanager decorator. This decorator transforms a simple generator function into a full-fledged context manager.

The structure is straightforward and elegant:

  1. Import contextmanager from contextlib.
  2. Define a generator function and decorate it with @contextmanager.
  3. The code before the yield statement is your setup logic (the __enter__ part).
  4. The yield statement passes control back to the with block. The value you yield becomes available to the as variable.
  5. The code after the yield statement is your teardown logic (the __exit__ part).

Let's see this in action.

Python Tutorial: Context Managers - Efficiently Managing Resources

Now, watch the next segment of the same video, where Corey Schafer refactors the class-based file manager into a generator-based one. The direct comparison makes the benefits immediately obvious.

Watch from 08:20 to 12:20. Pay close attention to how the try...finally block is used to wrap the yield statement. This is the critical pattern for ensuring the teardown code always runs, just like the __exit__ method.

The core pattern you just saw is essential:

from contextlib import contextmanager

@contextmanager
def my_context_manager():



    # 1. Setup logic runs here
    print("Setting up...")
    
    try:



        # 2. Yield to the 'with' block
        yield
    finally:



        # 3. Teardown logic runs here, guaranteed
        print("Tearing down...")




# Usage
with my_context_manager():
    print("Inside the 'with' block.")

This is significantly more readable than the class-based version for most use cases. The logical flow—setup, do the work, teardown—is linear and easy to follow.


3. A Practical Example: Temporarily Changing Directory

Re-implementing open() is a good learning exercise, but let's build something genuinely useful that isn't already a built-in. A common task in scripts is to temporarily change to a different directory, perform some actions, and then reliably change back, even if errors occur. This is a perfect candidate for a context manager.

Python Tutorial: Context Managers - Efficiently Managing Resources

The final part of Corey Schafer's video implements this exact 'change directory' context manager. It's an excellent, practical demonstration of creating a new, reusable tool.

Watch from 12:20 to 19:50. Note how this context manager doesn't need to yield a specific value, so it just uses a bare yield. This is common for context managers that only manage state or context without providing an object to work with.


4. Yielding a Value to the as Variable

In the change_dir example, we didn't need to pass an object into the with block. But what if you do? For example, you might want your context manager to provide a database connection, a temporary object, or a logging handle.

To do this, you simply pass a value with the yield statement.

The contextmanager decorator

The article "The contextmanager decorator" from Python Morsels provides a clear, text-based explanation of this pattern, along with other nuances.

First, read the introduction and the section "Turning functions into context managers" to solidify your understanding of the basic yield pattern. Then, skip down and read the section "Customizing what your context manager returns". It shows a practical example of yielding a logger object, which is then captured by the as variable in the with statement.


5. When to Stick with a Class

While @contextmanager is the preferred tool for its simplicity, it's not a silver bullet. For highly complex context managers, especially those that need to manage significant internal state or expose methods on the context manager object itself, a class can sometimes be clearer.

A general rule of thumb:

  • If your __enter__ and __exit__ logic is straightforward, use @contextmanager.
  • If your context manager needs to maintain complex state across the with block, or if you find yourself returning self from __enter__ to call methods on it, a class might be a better fit.

The contextmanager decorator

The Python Morsels article you just read concludes with a great discussion on this trade-off.

Read the section "When not to use the contextmanager decorator". The time_block example demonstrates a case where a class-based implementation is arguably just as clear as the decorator version, because it needs a helper class to store state.


Exercise: Create a Simple Timer Context Manager

Now it's your turn to build a context manager from scratch. Your task is to create a function time_block that can be used to time the execution of any block of code.

Requirements:

  1. The function should be a context manager created with the @contextmanager decorator.
  2. Upon entering the with block, it should record the start time using time.perf_counter().
  3. Upon exiting the with block, it should record the end time, calculate the duration, and print a formatted message like Block executed in: 0.1234 seconds.
  4. The cleanup (printing the time) must happen even if an exception occurs inside the with block.

Starter Code:

import time
from contextlib import contextmanager




# Your implementation here
@contextmanager
def time_block():



    # TODO: Implement the setup, yield, and teardown logic
    pass




# --- Example Usage ---

print("--- Timing a successful block ---")
with time_block():



    # Simulate some work
    time.sleep(0.5)

print("\n--- Timing a block with an error ---")
try:
    with time_block():
        time.sleep(0.2)
        raise ValueError("Something went wrong!")
except ValueError as e:
    print(f"Caught expected error: {e}")




# Check that the timer message still printed for the failed block.
Solution
import time
from contextlib import contextmanager

@contextmanager
def time_block():
    """A context manager to time a block of code."""
    start_time = time.perf_counter()
    try:



        # Yield control back to the with block
        yield
    finally:



        # This code is guaranteed to run
        end_time = time.perf_counter()
        elapsed_time = end_time - start_time
        print(f"Block executed in: {elapsed_time:.4f} seconds")




# --- Example Usage ---

print("--- Timing a successful block ---")
with time_block():



    # Simulate some work
    time.sleep(0.5)

print("\n--- Timing a block with an error ---")
try:
    with time_block():
        time.sleep(0.2)
        raise ValueError("Something went wrong!")
except ValueError as e:
    print(f"Caught expected error: {e}")

Notice how the try...finally structure ensures the timing message is printed in both the successful and the error-prone cases. You have created a robust, reusable tool for your development workflow.


Conclusion

You have now learned how to create your own context managers, a powerful feature for building robust and reusable APIs. By wrapping setup and teardown logic in a generator and using the @contextlib.contextmanager decorator, you can reduce boilerplate and make your code's intent clearer.

Key Takeaways:

  • Custom context managers automate setup and teardown logic for your own objects.
  • The @contextlib.contextmanager decorator turns a generator function into a context manager.
  • The code before yield is for setup, and the code after yield is for teardown.
  • Wrapping the yield in a try...finally block is the standard pattern to guarantee cleanup.
  • The value passed to yield is what gets assigned to the as variable in a with statement.
  • For simple, stateless context managers, the decorator is more Pythonic and concise than a full class.

In our next lesson, we will continue exploring the contextlib module by looking at other useful utilities like contextlib.suppress to cleanly ignore specific exceptions and contextlib.ExitStack to manage a dynamic number of contexts.