Skip to main content

Python Context Managers

Introduction

Have you ever forgotten to close a file after opening it? Or needed to ensure that a database connection is properly closed even if an error occurs? Python context managers are designed to solve these exact problems by providing a clean and efficient way to manage resources.

Context managers handle the setup and teardown of resources automatically, making your code cleaner, more readable, and less prone to resource leaks. In this tutorial, we'll explore how context managers work in Python, how to use built-in ones, and even how to create your own.

What Are Context Managers?

A context manager is a Python object that defines the methods __enter__() and __exit__() which allow you to execute code before and after a block of code. The most common way to use a context manager is with the with statement.

The general syntax looks like:

python
with context_manager_expression as variable:
# code that uses the resource
# when this block exits, the resource is automatically cleaned up

When the with statement executes:

  1. The context manager's __enter__() method is called
  2. The value returned by __enter__() is assigned to the variable after as
  3. The code block is executed
  4. The context manager's __exit__() method is called to clean up resources, even if an exception occurred

File Handling with Context Managers

The most common use of context managers is for file operations. Let's see the difference between traditional file handling and using context managers:

Without a Context Manager

python
# Without context manager
file = open('example.txt', 'w')
try:
file.write('Hello, World!')
finally:
file.close() # We must remember to close the file

With a Context Manager

python
# With context manager
with open('example.txt', 'w') as file:
file.write('Hello, World!')
# File is automatically closed when the block exits

The second approach is cleaner, more readable, and guarantees the file will be closed properly even if an exception occurs.

Built-in Context Managers

Python provides several built-in context managers that are extremely useful:

1. File Operations

We've already seen this one:

python
with open('example.txt', 'r') as f:
content = f.read()
print(content)

Output (assuming file contains "Hello, World!"):

Hello, World!

2. Managing Locks

When working with threads, locks help prevent race conditions:

python
import threading

lock = threading.Lock()

# Without context manager
lock.acquire()
try:
# Critical section
print("Lock acquired")
finally:
lock.release()

# With context manager
with lock:
# Critical section
print("Lock acquired with context manager")

Output:

Lock acquired
Lock acquired with context manager

3. Temporarily Changing Settings

The contextlib module provides useful context managers. For example, temporarily redirecting standard output:

python
import sys
from io import StringIO
from contextlib import redirect_stdout

with StringIO() as buffer, redirect_stdout(buffer):
print("This will be captured")

# Get the captured content
output = buffer.getvalue()

print(f"Captured: {output}")

Output:

Captured: This will be captured

Creating Your Own Context Managers

There are two ways to create custom context managers:

1. Using a Class

To create a class-based context manager, implement the __enter__ and __exit__ methods:

python
class MyContextManager:
def __init__(self, name):
self.name = name

def __enter__(self):
print(f"Entering context for {self.name}")
return self # The value returned here is assigned to the variable after 'as'

def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Exiting context for {self.name}")
# Return False to allow exceptions to propagate, True to suppress them
return False

# Using our custom context manager
with MyContextManager("example") as cm:
print(f"Inside the context block with {cm.name}")

Output:

Entering context for example
Inside the context block with example
Exiting context for example

2. Using the contextlib.contextmanager Decorator

For simpler cases, you can use a generator function with the @contextmanager decorator:

python
from contextlib import contextmanager

@contextmanager
def my_context_manager(name):
print(f"Entering context for {name}")
try:
# The yield statement separates setup from cleanup
yield name # This value is assigned to the variable after 'as'
finally:
# This runs when the context exits
print(f"Exiting context for {name}")

# Using our custom context manager
with my_context_manager("example") as name:
print(f"Inside the context block with {name}")

Output:

Entering context for example
Inside the context block with example
Exiting context for example

Practical Examples

Database Connection Management

Context managers are perfect for managing database connections:

python
import sqlite3
from contextlib import contextmanager

@contextmanager
def database_connection(db_name):
"""Context manager for SQLite database connections."""
conn = sqlite3.connect(db_name)
try:
yield conn
finally:
conn.close()

# Using the context manager
with database_connection('example.db') as conn:
cursor = conn.cursor()
cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)')
cursor.execute('INSERT INTO users (name) VALUES (?)', ('John',))
conn.commit()

# Query the data
cursor.execute('SELECT * FROM users')
print(cursor.fetchall())

Output:

[(1, 'John')]

Timing Code Execution

Let's create a context manager to measure how long code takes to run:

python
import time
from contextlib import contextmanager

@contextmanager
def timer(operation_name):
"""Context manager for timing code execution."""
start_time = time.time()
try:
yield
finally:
elapsed_time = time.time() - start_time
print(f"{operation_name} took {elapsed_time:.5f} seconds to complete")

# Using the timing context manager
with timer("Sleeping operation"):
# Simulate some work
time.sleep(1)

Output:

Sleeping operation took 1.00123 seconds to complete

Temporarily Changing Working Directory

Here's a context manager to temporarily change the working directory:

python
import os
from contextlib import contextmanager

@contextmanager
def change_directory(new_dir):
"""Temporarily change the working directory."""
original_dir = os.getcwd()
try:
os.chdir(new_dir)
yield
finally:
os.chdir(original_dir)

# Using the context manager
print(f"Current directory: {os.getcwd()}")
with change_directory('/tmp'):
print(f"Inside context manager: {os.getcwd()}")
print(f"After context manager: {os.getcwd()}")

Output (on a Unix system):

Current directory: /home/user
Inside context manager: /tmp
After context manager: /home/user

Context Managers with Exception Handling

One powerful feature of context managers is their ability to handle exceptions that occur within the with block:

python
class HandleException:
def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"An exception occurred: {exc_val}")
# Return True to suppress the exception
return True
return False

# Using our exception handling context manager
with HandleException():
print("About to divide by zero...")
result = 1 / 0
print("This line won't be reached")

print("Continuing execution despite the exception")

Output:

About to divide by zero...
An exception occurred: division by zero
Continuing execution despite the exception

Nested Context Managers

Context managers can be nested, and Python ensures that each one is exited in the correct order:

python
with open('file1.txt', 'w') as file1:
file1.write('File 1 content')
with open('file2.txt', 'w') as file2:
file2.write('File 2 content')
print("Both files are open")
print("Only file1 is still open")
print("All files are closed")

Output:

Both files are open
Only file1 is still open
All files are closed

You can also use a more compact syntax for multiple context managers:

python
with open('file1.txt', 'w') as file1, open('file2.txt', 'w') as file2:
file1.write('File 1 content')
file2.write('File 2 content')
print("Both files are open")
print("All files are closed")

Output:

Both files are open
All files are closed

Summary

Context managers are a powerful feature in Python that help you properly manage resources by ensuring cleanup code is always executed. They make your code cleaner, more readable, and more robust by automating resource management.

Key takeaways:

  • Use the with statement to work with context managers
  • Built-in context managers include open(), threading.Lock(), and more
  • Create custom context managers using either classes or the @contextmanager decorator
  • Context managers automatically handle resource cleanup, even when exceptions occur
  • They make your code more robust and less prone to resource leaks

Exercises

  1. Write a context manager that suppresses specific exceptions (similar to contextlib.suppress).
  2. Create a context manager that redirects the standard error output to a file.
  3. Implement a "transaction" context manager for a simple key-value store that rolls back changes if an exception occurs.
  4. Write a context manager that temporarily sets an environment variable and restores the original value when exiting.
  5. Create a context manager that measures and logs memory usage before and after a block of code executes.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)