Back to Blog
Lesson 39 of the Python: Programming from Zero with Python course
PythonAugust 26, 20264 min read

Mastering Python Context Managers and the with Statement

Learn to build custom context managers in Python. Understand __enter__ and __exit__ methods to handle resource management and ensure clean code execution.

pythoncontext-managersresource-managementclean-codeprogramming
Young professional woman working on a laptop in an office setting, concentrating on her task.

Previously in this course, we used the with statement to handle file operations in Reading Files in Python: A Guide to File I/O and Context Managers. While that taught us how to consume existing tools, today we’ll take a step further: we are going to build our own context managers from scratch.

Understanding Resource Management

In backend development, resource management is the practice of ensuring that every resource you open—a database connection, a network socket, or even a temporary file—is properly closed after use. If you forget to close these, your application will eventually leak resources, leading to crashes or "too many open files" errors.

A context manager is a Python object that defines the runtime context to be established when executing a with statement. It encapsulates the "setup" phase (acquiring the resource) and the "teardown" phase (releasing the resource), guaranteeing that the teardown happens even if an error occurs.

The Anatomy of a Context Manager

To create a custom context manager, you define a class with two special magic methods: __enter__ and __exit__.

  1. __enter__(self): This method is called when the with block begins. It should perform the setup logic and return the resource you want to use.
  2. __exit__(self, exc_type, exc_value, traceback): This method is called when the with block ends, whether it finished successfully or raised an exception. It performs the cleanup logic.

A Worked Example: A Simple Timer

Let's build a Timer class that logs how long a specific block of code takes to execute. This is a classic example of a context manager that doesn't just handle file I/O, but handles time as a resource.

PYTHON
import time

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end = time.perf_counter()
        self.duration = self.end - self.start
        print(f"Code block took {self.duration:.4f} seconds to run.")

# Using our custom context manager
with Timer():
    # Simulate some work
    time.sleep(1.5)

In this example, when with Timer() is called, Python executes __enter__. When the indentation level returns to the original scope, Python automatically triggers __exit__, calculating the elapsed time.

Why Use Custom Context Managers?

Beyond simple timers, you will use these for:

  • Database Connections: Opening a connection and ensuring it closes even if a query fails.
  • Locking: Acquiring a thread lock and ensuring it releases.
  • API Client Sessions: Managing headers or temporary authentication tokens.

Hands-on Exercise: A Database Simulator

Create a class called DatabaseConnection.

  1. In __enter__, print "Connecting to database..." and return the string "Connected".
  2. In __exit__, print "Closing connection...".
  3. Write a with statement that assigns the result of DatabaseConnection() to a variable named db and prints it.
PYTHON
# Exercise Template
class DatabaseConnection:
    def __enter__(self):
        # Your code here
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        # Your code here
        pass

# Test your implementation
with DatabaseConnection() as db:
    print(f"Doing work with: {db}")

Common Pitfalls

  • Forgetting to return a value: If you want to use the resource inside the with block (e.g., with MyManager() as resource:), your __enter__ method must return that resource.
  • Ignoring Exceptions: Your __exit__ method receives exception details. If you want to suppress an exception (prevent it from bubbling up), you must return True from __exit__. By default, it returns None, which means the exception will continue to propagate.
  • Over-engineering: Don't build a class if a simple try...finally block is sufficient. Context managers are for reusable logic.

FAQ

Q: Can I use functions instead of classes for context managers? A: Yes, using the @contextlib.contextmanager decorator, you can write a generator function that yields a value. We will explore this in a more advanced lesson.

Q: Does __exit__ run if the program crashes? A: It runs if an exception is raised within the with block. If the entire Python process is killed (e.g., kill -9 or a power failure), the __exit__ method will not run.

Recap

Context managers are the backbone of clean, safe resource management in Python. By defining __enter__ and __exit__, you ensure that your setup and cleanup logic are tightly coupled and guaranteed to execute. This is an essential pattern for building robust CLI tools and APIs, much like the Persistent CLI Tool we built earlier in this course.

Up next: We will look at Generators to learn how to handle large data sets without consuming all your computer's memory.

Similar Posts