Context managers in Python are powerful tools for managing resources like files, network connections, or database connections. They ensure that setup actions (like opening a file) are performed automatically when entering a "context" and teardown actions (like closing a file) are performed automatically when exiting that context, even if errors occur. This helps prevent resource leaks and makes code cleaner and more robust. Python's with
statement is the primary way to utilize context managers. Understanding context managers is crucial for writing efficient and reliable Python applications.
Example 1: Basic File Handling with with
(Beginner-Friendly)
# Open a file for writing using a context manager
with open("my_file.txt", "w") as file:
file.write("Hello, Python context managers!\n")
file.write("This line is also written to the file.")
# The file is automatically closed after exiting the 'with' block
print("File 'my_file.txt' has been written to and closed.")
Explanation: This example demonstrates the most common use of the with
statement: file handling. We open my_file.txt
in write mode ("w"
). The as file
part assigns the opened file object to the variable file
. Inside the with
block, we write two lines to the file. Crucially, once the code exits the with
block (either normally or due to an error), Python automatically calls the file's __exit__
method, ensuring the file is properly closed, preventing potential resource leaks. This is a fundamental concept for managing file I/O in Python.
Example 2: Reading from a File with with
and Error Handling (Intermediate)
try:
with open("non_existent_file.txt", "r") as file:
content = file.read()
print(f"File content:\n{content}")
except FileNotFoundError:
print("Error: The specified file was not found.")
finally:
print("Attempted to read file (whether successful or not).")
print("\n--- Reading an existing file ---")
# Create a dummy file for the next part of the example
with open("existing_file.txt", "w") as f:
f.write("Line 1\nLine 2\nLine 3")
with open("existing_file.txt", "r") as file:
lines = file.readlines()
print("Lines from existing_file.txt:")
for line in lines:
print(line.strip()) # .strip() removes leading/trailing whitespace including newlines
Explanation: This example expands on file handling. The first part shows how with
interacts with error handling (try-except
). Even if a FileNotFoundError
occurs, the with
statement ensures no unclosed file handles are left behind. The finally
block demonstrates code that will always execute regardless of whether an exception occurred. The second part demonstrates reading an existing file line by line, showcasing common file reading patterns with context managers. This highlights the robustness of with
statements even in the presence of potential issues.
Example 3: Managing Network Connections (Advanced Concept - Pseudocode)
import socket
class NetworkConnection:
def __init__(self, host, port):
self.host = host
self.port = port
self.sock = None
def __enter__(self):
print(f"Attempting to connect to {self.host}:{self.port}...")
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
print("Connection established.")
return self.sock
def __exit__(self, exc_type, exc_val, exc_tb):
if self.sock:
self.sock.close()
print("Connection closed.")
# Returning False propagates the exception, True suppresses it
return False
# This is illustrative. In a real scenario, you'd replace 'localhost' and 8080 with actual values
# try:
# with NetworkConnection('localhost', 8080) as sock:
# sock.sendall(b"Hello from client!")
# response = sock.recv(1024)
# print(f"Received: {response.decode()}")
# except ConnectionRefusedError:
# print("Connection refused. Is the server running?")
# except Exception as e:
# print(f"An unexpected error occurred: {e}")
print("Note: The above network connection code is illustrative and requires a running server to function fully.")
print("It demonstrates how a custom context manager could manage network resources.")
Explanation: While this specific network connection code is illustrative (it requires a running server to fully execute), it serves as an excellent conceptual example of how context managers can be used for more complex resource management beyond files. It shows the structure of a custom context manager (NetworkConnection
class) and how __enter__
sets up the connection and __exit__
ensures it's properly closed, even if errors occur during data transmission. This demonstrates the power of context managers for managing external resources like sockets, ensuring proper cleanup.
Example 4: Database Connection Management (Advanced Concept - Pseudocode)
# Imagine a simplified database connection class
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"Connecting to database: {self.db_name}...")
# Simulate opening a database connection
# In a real app, this would be psycopg2.connect(), sqlite3.connect(), etc.
self.connection = f"Connection object for {self.db_name}"
print("Database connection established.")
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
print(f"Closing database connection: {self.db_name}...")
# Simulate closing the connection
# In a real app, this would be self.connection.close()
self.connection = None
print("Database connection closed.")
# Returning False propagates any exceptions that occurred within the 'with' block
return False
# This is illustrative. Replace with actual database interactions
# try:
# with DatabaseConnection('my_application_db') as db:
# # Execute SQL queries here
# print(f"Performing operations with: {db}")
# # db.execute("INSERT INTO users (name) VALUES ('Alice')")
# # db.commit()
# except Exception as e:
# print(f"Database error: {e}")
print("Note: This database connection example is a simplified illustration.")
print("It demonstrates the pattern of using context managers for database resource management.")
Explanation: Similar to the network connection example, this pseudocode illustrates how a custom context manager could be used to manage database connections. Establishing and closing database connections are critical operations that often involve boilerplate code and can lead to resource leaks if not handled carefully. By encapsulating these operations within __enter__
and __exit__
, the with
statement ensures that connections are always properly opened and closed, making your database interactions more robust and easier to manage, a key benefit for web development and data science applications.
Example 5: Resource Pooling with Context Managers (Advanced)
import threading
import time
class Resource:
def __init__(self, id):
self.id = id
self.is_available = True
print(f"Resource {self.id} created.")
def use(self):
print(f"Resource {self.id} is being used.")
time.sleep(0.1) # Simulate some work
def release(self):
self.is_available = True
print(f"Resource {self.id} released.")
class ResourcePool:
def __init__(self, size):
self.pool = [Resource(i) for i in range(size)]
self.lock = threading.Lock()
def acquire(self):
with self.lock:
for resource in self.pool:
if resource.is_available:
resource.is_available = False
print(f"Resource {resource.id} acquired from pool.")
return resource
raise RuntimeError("No resources available in the pool.")
def release(self, resource):
with self.lock:
resource.is_available = True
print(f"Resource {resource.id} returned to pool.")
def __enter__(self):
# When entering the 'with' block for the pool itself, we don't return a resource yet
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# No specific cleanup for the pool itself on exit,
# individual resources are managed by acquire/release
pass
# A nested context manager for acquiring individual resources from the pool
class ManagedResource:
def __init__(self, pool_instance):
self.pool = pool_instance
self.resource = None
def __enter__(self):
self.resource = self.pool.acquire()
return self.resource
def __exit__(self, exc_type, exc_val, exc_tb):
if self.resource:
self.pool.release(self.resource)
return False # Propagate exceptions
# Usage
resource_pool = ResourcePool(size=3)
# Simulating multiple threads trying to acquire resources
def worker(thread_id):
print(f"Thread {thread_id}: trying to get a resource...")
try:
# Use the nested context manager for resource acquisition
with resource_pool.ManagedResource(resource_pool) as res:
res.use()
if thread_id == 1:
# Simulate an error to show release still happens
print(f"Thread {thread_id}: Simulating an error!")
raise ValueError("Simulated error")
except RuntimeError as e:
print(f"Thread {thread_id}: Error acquiring resource - {e}")
except ValueError as e:
print(f"Thread {thread_id}: Caught simulated error - {e}")
finally:
print(f"Thread {thread_id}: Finished processing.")
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
time.sleep(0.05) # Stagger thread start times
for t in threads:
t.join()
print("\nAll threads finished. Pool resources should be released.")
Explanation: This advanced example demonstrates using context managers to implement a simple resource pool. The ResourcePool
class manages a collection of Resource
objects. The key innovation here is the nested ManagedResource
context manager within ResourcePool
. When a ManagedResource
is entered (__enter__
), it acquires an available resource from the pool. When it's exited (__exit__
), it ensures that the resource is returned to the pool, even if the worker
function encounters an error. This pattern is invaluable for managing limited resources in concurrent environments (like database connection pools or thread pools), making your applications more scalable and efficient.
with statement revisited
Explanation: The with
statement in Python is a fundamental control flow structure used for simplifying the management of resources. It guarantees that a specific setup action is performed when entering a block of code and a corresponding teardown action is performed when exiting, regardless of whether the block completes successfully or an error occurs. This pattern, often referred to as the "context management protocol," ensures proper resource cleanup, preventing issues like file leaks, unclosed network connections, or uncommitted database transactions. Mastering the with
statement is essential for writing robust and reliable Python code, particularly when dealing with I/O operations and external systems. It significantly reduces boilerplate code and improves code readability by centralizing resource handling.
Example 1: Basic File Writing with with
(Beginner-Friendly)
# Using the 'with' statement for writing to a file is the standard Pythonic way.
with open("simple_log.txt", "w") as log_file:
log_file.write("Application started successfully.\n")
log_file.write("Processing user request ID: 12345.\n")
# Even if an error occurs here, log_file will be closed.
print("Log messages written. File 'simple_log.txt' is now closed.")
Explanation: This example reiterates the core benefit of the with
statement: automatic resource management. We open simple_log.txt
in write mode. The with
statement ensures that log_file
(the file object) is automatically closed once the code leaves the with
block, even if an exception were to occur during the write
operations. This prevents resource leaks and simplifies error handling compared to manual try-finally
blocks. This is a common pattern for managing file I/O in Python.
Example 2: Reading a CSV File (Intermediate)
import csv
# Create a dummy CSV file for demonstration
with open("data.csv", "w", newline='') as f:
writer = csv.writer(f)
writer.writerow(["Name", "Age", "City"])
writer.writerow(["Alice", 30, "New York"])
writer.writerow(["Bob", 24, "London"])
# Now, read the CSV file using 'with'
data = []
with open("data.csv", "r", newline='') as csv_file:
csv_reader = csv.reader(csv_file)
header = next(csv_reader) # Read header row
for row in csv_reader:
data.append(row)
print("CSV Header:", header)
print("CSV Data:", data)
print("File 'data.csv' has been read and closed.")
Explanation: This example demonstrates the with
statement's utility when working with the csv
module. By opening the CSV file within a with
block, we ensure that the file is properly closed after reading, regardless of whether the reading process completes successfully or an error occurs (e.g., malformed data). This is crucial for data processing and data science tasks where file integrity and resource management are paramount.
Example 3: Locking in Multithreaded Applications (Advanced)
import threading
import time
shared_data = 0
# A Lock is a basic synchronization primitive.
# It's also a context manager!
lock = threading.Lock()
def increment_data(thread_id):
global shared_data
print(f"Thread {thread_id}: Trying to acquire lock...")
# The 'with' statement ensures the lock is acquired before entering
# and released automatically when exiting, even on errors.
with lock:
print(f"Thread {thread_id}: Lock acquired. Current data: {shared_data}")
local_copy = shared_data
time.sleep(0.01) # Simulate some work
shared_data = local_copy + 1
print(f"Thread {thread_id}: Lock released. New data: {shared_data}")
threads = []
for i in range(5):
t = threading.Thread(target=increment_data, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"\nFinal shared data value: {shared_data}")
Explanation: This example showcases how with
statements are used with threading.Lock
objects to prevent race conditions in multithreaded applications. When with lock:
is used, the lock is automatically acquired before the block is entered and automatically released when the block is exited (either normally or due to an exception). This guarantees that only one thread can access the shared_data
at a time, ensuring data integrity. This is a critical pattern for concurrent programming and building scalable Python applications.
Example 4: Mocking with unittest.mock.patch
(Advanced Testing)
from unittest.mock import patch
# Imagine a function that interacts with a database or external API
def get_user_data(user_id):
# In a real scenario, this would hit a database
print(f"Fetching actual data for user {user_id} from database...")
if user_id == 1:
return {"id": 1, "name": "Alice", "email": "alice@example.com"}
return None
def process_user_info(user_id):
user_data = get_user_data(user_id)
if user_data:
return f"User: {user_data['name']}, Email: {user_data['email']}"
return "User not found."
# Using 'with patch' to temporarily replace 'get_user_data' for testing
print("--- Testing with original function ---")
print(process_user_info(1))
print(process_user_info(99))
print("\n--- Testing with mocked function ---")
with patch(__name__ + '.get_user_data') as mock_get_user_data:
# Configure the mock's return value
mock_get_user_data.return_value = {"id": 100, "name": "Mock User", "email": "mock@test.com"}
print(process_user_info(100)) # This will use the mocked return value
mock_get_user_data.return_value = None
print(process_user_info(200)) # This will use the new mocked return value
# After the 'with' block, get_user_data is restored to its original implementation
print("\n--- After patch, back to original function ---")
print(process_user_info(1))
Explanation: This example demonstrates the with
statement's power in testing, specifically with Python's unittest.mock.patch
. patch
itself is a context manager. When you enter the with patch(...)
block, the specified object (get_user_data
in this case) is temporarily replaced with a mock object. This allows you to control the behavior of external dependencies during testing, without actually hitting a database or making network calls. When the with
block exits, the original object is automatically restored, ensuring test isolation. This is an essential technique for writing robust and maintainable unit tests in Python.
Example 5: Suppressing Exceptions with contextlib.suppress
(Advanced)
import os
from contextlib import suppress
file_to_delete = "temp_file_for_deletion.txt"
# Create a dummy file
with open(file_to_delete, "w") as f:
f.write("This file will be deleted.")
print(f"File '{file_to_delete}' created.")
# Attempt to remove the file, suppressing FileNotFoundError if it doesn't exist
print("\nAttempting to delete the file (first time)...")
with suppress(FileNotFoundError):
os.remove(file_to_delete)
print(f"Successfully deleted '{file_to_delete}'.")
print("\nAttempting to delete the file again (it's already gone)...")
# The FileNotFoundError will be suppressed here
with suppress(FileNotFoundError):
os.remove(file_to_delete)
print(f"Tried to delete '{file_to_delete}' again, but it was already gone. No error shown.")
# You won't see a FileNotFoundError traceback here because it's suppressed.
print("\nCode continues execution after suppression.")
# Demonstrate with a different error that is NOT suppressed
print("\n--- Demonstrating unsuppressed error ---")
try:
with suppress(ValueError): # Only suppressing ValueError, not TypeError
x = 1 / 0 # This will raise ZeroDivisionError
except ZeroDivisionError:
print("Caught ZeroDivisionError as expected (not suppressed).")
Explanation: This example introduces contextlib.suppress
, a specialized context manager that allows you to gracefully ignore specific exceptions. When an exception specified in suppress()
occurs within the with
block, it is "swallowed" (suppressed), and execution continues as if no exception occurred. If any other type of exception occurs, it will not be suppressed. This can be useful for scenarios where you anticipate certain errors but don't want them to interrupt program flow, such as attempting to delete a file that might or might not exist. It's a convenient shorthand for certain try-except
blocks.
Creating custom context managers (__enter__
, __exit__
)
While the with
statement is a built-in Python feature, its power comes from the ability to define your own objects that can act as context managers. This is achieved by implementing two special methods within a class: __enter__
and __exit__
. The __enter__
method is called when execution enters the with
block, and it typically performs setup actions and returns the resource to be used. The __exit__
method is called when execution leaves the with
block (either normally or due to an exception), and it handles teardown actions, ensuring resources are properly released. This powerful pattern allows you to encapsulate complex resource management logic, making your Python code more modular, readable, and robust for a variety of tasks, from file handling to managing external API connections.
Example 1: Simple Timer Context Manager (Beginner-Friendly)
import time
class SimpleTimer:
def __enter__(self):
self.start_time = time.time()
print("Timer started...")
return self # We don't return a specific resource to work with here
def __exit__(self, exc_type, exc_val, exc_tb):
end_time = time.time()
duration = end_time - self.start_time
print(f"Timer stopped. Elapsed time: {duration:.4f} seconds.")
# Returning False propagates any exception that occurred
return False
# Use the custom timer
print("--- Using SimpleTimer ---")
with SimpleTimer():
# Simulate some work
print("Doing some work inside the timed block...")
time.sleep(0.5)
print("Work done.")
print("\n--- Another timed block ---")
with SimpleTimer():
print("Doing more work...")
time.sleep(0.2)
# Simulate an error to show __exit__ still runs
# raise ValueError("Oops, something went wrong!")
print("More work done.")
print("\nProgram finished.")
Explanation: This example demonstrates a very basic custom context manager for timing code execution. The __enter__
method records the start time. The __exit__
method calculates and prints the duration. Notice that __exit__
receives arguments related to any exception that occurred within the with
block (exc_type
, exc_val
, exc_tb
). By returning False
, we ensure that if an exception did occur, it would be re-raised after __exit__
finishes, which is the standard behavior. This simple timer is a great way to understand the core mechanics of __enter__
and __exit__
.
Example 2: Managed File Writer with Header/Footer (Intermediate)
class ManagedFileWriter:
def __init__(self, filename, mode='w'):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print(f"Opening file '{self.filename}' in '{self.mode}' mode...")
self.file = open(self.filename, self.mode)
self.file.write("--- START OF LOG ---\n") # Write a header
return self.file # Return the file object for use in the 'with' block
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.write("--- END OF LOG ---\n") # Write a footer
self.file.close()
print(f"File '{self.filename}' closed.")
# If an exception occurred, we print a message but still propagate it
if exc_type:
print(f"An exception of type {exc_type.__name__} occurred.")
return False # Propagate exception if one occurred
# Usage
with ManagedFileWriter("custom_log.txt", "w") as f:
f.write("This is a log entry.\n")
f.write("Another important message.\n")
# You could simulate an error here:
# int("abc")
print("\n--- Reading the generated custom_log.txt ---")
with open("custom_log.txt", "r") as f_read:
print(f_read.read())
Explanation: This custom context manager builds on the file handling example. ManagedFileWriter
automatically adds a header and a footer to the file content. The __enter__
method opens the file and writes the header, returning the file object so it can be used within the with
block. The __exit__
method writes the footer and ensures the file is closed. This demonstrates how you can wrap standard resource operations with additional logic, making your file operations more structured and robust.
Example 3: Suppressing Specific Exceptions (Custom Implementation - Advanced)
class SuppressErrors:
def __init__(self, *exception_types):
self.exception_types = exception_types
self.suppressed_exception = None
def __enter__(self):
# Nothing to set up specifically on enter
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type in self.exception_types:
print(f"Caught and suppressed {exc_type.__name__}: {exc_val}")
self.suppressed_exception = exc_val # Store the suppressed exception
return True # Return True to suppress the exception
return False # Return False to propagate other exceptions
# Usage
print("--- Suppressing ValueError ---")
with SuppressErrors(ValueError):
x = int("hello") # This will raise ValueError
print("This line will not be reached if ValueError is raised.")
print("Execution continues after suppressed error.")
print("\n--- Not suppressing TypeError ---")
try:
with SuppressErrors(ValueError): # Only suppressing ValueError
my_dict = {"a": 1}
print(my_dict["b"] + "hello") # This will raise KeyError and then TypeError
except (KeyError, TypeError) as e:
print(f"Caught and propagated an unsuppressed error: {type(e).__name__}: {e}")
print("\n--- Suppressing multiple error types ---")
with SuppressErrors(TypeError, ZeroDivisionError):
result = 10 / 0 # This will raise ZeroDivisionError
print("This line is not reached.")
print("Execution continues after multiple suppressed errors.")
Explanation: This example implements a custom version of exception suppression, similar to contextlib.suppress
. The SuppressErrors
context manager takes one or more exception types in its constructor. In __exit__
, it checks if the raised exception matches any of the specified types. If it does, it prints a message and returns True
, which tells Python to suppress the exception. If the exception type does not match, it returns False
, allowing the exception to propagate. This showcases how __exit__
can be used to control the flow of exceptions, a powerful feature for error handling strategies.
Example 4: Managing a Simulated Transaction (Advanced)
class Transaction:
def __init__(self, db_connection_info):
self.db_connection_info = db_connection_info
self.connection = None
self.in_transaction = False
def __enter__(self):
print(f"Connecting to DB: {self.db_connection_info}...")
# Simulate opening a connection and starting a transaction
self.connection = f"DB_CONN_OBJECT_{self.db_connection_info}"
print("Transaction started.")
self.in_transaction = True
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.in_transaction:
if exc_type is None:
print("No errors. Committing transaction.")
# Simulate commit
else:
print(f"Error detected ({exc_type.__name__}). Rolling back transaction.")
# Simulate rollback
self.in_transaction = False
print("Disconnecting from DB.")
# Simulate closing connection
self.connection = None
return False # Always propagate exceptions after handling transaction
# Usage
print("--- Successful Transaction ---")
with Transaction("ProductionDB") as db:
print(f"Using database connection: {db}")
print("Performing operation 1...")
print("Performing operation 2...")
print("\n--- Transaction with an Error ---")
try:
with Transaction("StagingDB") as db:
print(f"Using database connection: {db}")
print("Performing operation A...")
raise ValueError("Simulated database error during operation B!")
print("Performing operation B...") # This line won't be reached
except ValueError as e:
print(f"Caught expected error outside transaction: {e}")
print("\n--- Another successful transaction ---")
with Transaction("TestDB") as db:
print("Doing a final successful operation.")
Explanation: This example demonstrates a more complex use case: managing a database-like transaction. The Transaction
context manager encapsulates the logic for starting a transaction (in __enter__
) and either committing or rolling back based on whether an exception occurred within the with
block (in __exit__
). If exc_type
is None
, it means no exception occurred, and the transaction is committed. Otherwise, it's rolled back. This pattern is crucial for ensuring data consistency in applications that interact with databases or other transactional systems, making your data operations reliable.
Example 5: Resource Cleanup with Conditional Logging (Advanced)
import logging
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
class TempResource:
def __init__(self, name):
self.name = name
self.is_active = False
def __enter__(self):
logging.info(f"Acquiring temporary resource: {self.name}")
# Simulate resource setup
self.is_active = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.is_active:
logging.info(f"Releasing temporary resource: {self.name}")
# Simulate resource cleanup
self.is_active = False
if exc_type:
logging.error(f"Error during resource usage: {exc_type.__name__}: {exc_val}")
else:
logging.warning(f"Resource {self.name} was not active, no release needed.")
return False # Propagate exceptions
# Usage
print("--- Normal resource usage ---")
with TempResource("CacheManager"):
logging.info("Using CacheManager to process data...")
# Simulate some data processing
pass
print("\n--- Resource usage with an error ---")
try:
with TempResource("NetworkClient"):
logging.info("Sending data via NetworkClient...")
raise ConnectionError("Failed to connect to server!")
logging.info("Data sent.") # This line won't be reached
except ConnectionError:
logging.info("Caught ConnectionError outside the context manager.")
print("\n--- Resource that might not be active (e.g., already cleaned up elsewhere) ---")
# Manually create but don't enter context for demonstration of warning
resource_obj = TempResource("PreExistingResource")
resource_obj.is_active = False # Explicitly set to False for this demo
print("Attempting to exit a resource that wasn't properly entered (for demo):")
# Directly call __exit__ (not typical usage, just for demonstration)
resource_obj.__exit__(None, None, None)
Explanation: This advanced example combines custom context managers with conditional logging. The TempResource
class simulates the acquisition and release of a temporary resource. Within its __exit__
method, it not only ensures cleanup but also logs messages based on whether an error occurred (logging.error
) or if the resource was somehow not active (logging.warning
). This pattern is highly useful for debugging and monitoring complex systems, providing clear insights into resource lifecycle events and potential issues. It showcases how context managers can integrate with logging frameworks to provide comprehensive operational visibility.
Using contextlib
module (@contextmanager
)
While creating custom context managers by implementing __enter__
and __exit__
is powerful, it can be verbose for simpler cases. Python's contextlib
module provides a more concise and elegant way to create context managers using a decorator: @contextmanager
. This decorator allows you to write a generator function that yields exactly once. The code before the yield
statement acts as the __enter__
part (setup), and the code after the yield
acts as the __exit__
part (teardown). This functional approach simplifies the creation of context managers, making your code cleaner and more readable, especially for one-off or less complex resource management scenarios. It's a highly recommended pattern for Python developers.
Example 1: Simple File Handling with @contextmanager
(Beginner-Friendly)
from contextlib import contextmanager
@contextmanager
def open_managed_file(filename, mode):
file = None
try:
print(f"Opening file '{filename}' in '{mode}' mode...")
file = open(filename, mode)
yield file # This is where the file object is yielded to the 'with' block
finally:
if file:
file.close()
print(f"File '{filename}' closed.")
# Usage
print("--- Writing with @contextmanager ---")
with open_managed_file("managed_file.txt", "w") as f:
f.write("Hello from @contextmanager!\n")
f.write("This is a cleaner way to define context managers.")
print("\n--- Reading with @contextmanager ---")
with open_managed_file("managed_file.txt", "r") as f_read:
content = f_read.read()
print("Content of managed_file.txt:\n", content)
Explanation: This example demonstrates the most common and intuitive use of @contextmanager
. The open_managed_file
function is decorated, turning it into a context manager. The try-finally
block ensures that file.close()
is called regardless of whether errors occur. The yield file
statement is crucial: everything before it runs when entering the with
block, and the file
object is returned to the as f
variable. Everything after yield
runs when exiting the with
block. This approach provides a clear and concise way to manage file resources.
Example 2: Timing Code Execution with @contextmanager
(Intermediate)
from contextlib import contextmanager
import time
@contextmanager
def timer(name=""):
start_time = time.time()
print(f"Timer '{name}' started...")
try:
yield # No specific resource to yield, just provides a context
finally:
end_time = time.time()
duration = end_time - start_time
print(f"Timer '{name}' stopped. Elapsed time: {duration:.4f} seconds.")
# Usage
print("--- Timing a simple operation ---")
with timer("my_operation"):
sum_val = 0
for i in range(1000000):
sum_val += i
print(f"Sum calculated: {sum_val}")
print("\n--- Timing another operation with potential error ---")
try:
with timer("error_prone_task"):
print("Starting task...")
time.sleep(0.1)
raise RuntimeError("Simulated error during task!")
print("Task finished.") # This line won't be reached
except RuntimeError as e:
print(f"Caught expected error: {e}")
Explanation: This example re-implements the SimpleTimer
using @contextmanager
. The timer
function doesn't yield a specific resource, but rather just provides a contextual block. The yield
statement effectively splits the function into two parts: setup (before yield
) and teardown (after yield
). The try-finally
block is important here to ensure the timer always reports the duration, even if an error occurs within the with
block. This showcases how @contextmanager
simplifies creating functional context managers for various purposes like performance monitoring.
Example 3: Database Cursor Management with @contextmanager
(Advanced)
from contextlib import contextmanager
# Simulate a database connection and cursor
class MockConnection:
def cursor(self):
print("Creating mock cursor...")
return MockCursor()
def close(self):
print("Mock connection closed.")
def commit(self):
print("Mock transaction committed.")
def rollback(self):
print("Mock transaction rolled back.")
class MockCursor:
def execute(self, query):
print(f"Executing query: {query}")
if "ERROR" in query:
raise ValueError("Simulated SQL error!")
def fetchall(self):
print("Fetching all results.")
return [("data1",), ("data2",)]
def close(self):
print("Mock cursor closed.")
@contextmanager
def db_transaction(connection):
cursor = None
try:
cursor = connection.cursor()
yield cursor # Yield the cursor for SQL operations
connection.commit()
print("Transaction committed.")
except Exception as e:
connection.rollback()
print(f"Transaction rolled back due to error: {e}")
raise # Re-raise the exception after rollback
finally:
if cursor:
cursor.close()
print("Cursor closed.")
# Usage
conn = MockConnection()
print("--- Successful DB Transaction ---")
try:
with db_transaction(conn) as cursor:
cursor.execute("SELECT * FROM users")
results = cursor.fetchall()
print("Query Results:", results)
except Exception as e:
print(f"Caught an unexpected error: {e}")
print("\n--- Failed DB Transaction ---")
try:
with db_transaction(conn) as cursor:
cursor.execute("INSERT INTO orders VALUES ('INVALID_DATA_ERROR')") # Simulate an error
cursor.execute("SELECT * FROM products") # This won't be reached
except ValueError as e:
print(f"Caught the expected SQL error: {e}")
conn.close() # Close the mock connection at the end
Explanation: This advanced example showcases how @contextmanager
can be used to manage database transactions and cursors. The db_transaction
function yields a database cursor. If any error occurs within the with
block, the except
block catches it, rolls back the transaction, and then re-raises the exception. If no error occurs, the transaction is committed. The finally
block ensures the cursor is always closed. This pattern is incredibly useful for ensuring data integrity and proper resource cleanup when interacting with databases in Python applications, and is a common practice in web frameworks like Django and Flask.
Example 4: Changing Current Working Directory Temporarily (Advanced)
from contextlib import contextmanager
import os
@contextmanager
def change_directory(new_path):
old_path = os.getcwd()
print(f"Changing directory from '{old_path}' to '{new_path}'")
try:
os.chdir(new_path)
yield # The context is simply the new directory
finally:
os.chdir(old_path)
print(f"Restoring directory to '{old_path}'")
# Create a temporary directory for demonstration
os.makedirs("my_temp_dir", exist_ok=True)
with open(os.path.join("my_temp_dir", "temp_file.txt"), "w") as f:
f.write("Content in temporary directory.")
print("--- Current directory before change:", os.getcwd())
with change_directory("my_temp_dir"):
print("Inside context manager. Current directory:", os.getcwd())
# You can now operate on files relative to 'my_temp_dir'
with open("temp_file.txt", "r") as f:
print("Content of temp_file.txt:", f.read())
# Simulate an error to ensure directory is still restored
# raise OSError("Simulated permission error!")
print("Current directory after context manager:", os.getcwd())
# Clean up the temporary directory and file
os.remove(os.path.join("my_temp_dir", "temp_file.txt"))
os.rmdir("my_temp_dir")
Explanation: This practical example uses @contextmanager
to temporarily change the current working directory. The __enter__
part saves the original directory and changes to the new one. The __exit__
part (code after yield
) always changes back to the original directory, even if errors occur within the with
block. This is extremely useful for scripts that need to perform operations in specific directories without permanently altering the global working directory, common in automation scripts and build systems.
Example 5: Suppressing Output to Console (Advanced)
from contextlib import contextmanager
import sys
import os
@contextmanager
def suppress_stdout_stderr():
# Save the original stdout and stderr
original_stdout = sys.stdout
original_stderr = sys.stderr
# Redirect stdout and stderr to a null device (e.g., /dev/null on Unix, NUL on Windows)
devnull = open(os.devnull, 'w')
sys.stdout = devnull
sys.stderr = devnull
try:
yield # Code inside 'with' block will have its output suppressed
finally:
# Restore original stdout and stderr
sys.stdout = original_stdout
sys.stderr = original_stderr
devnull.close()
print("\nOutput streams restored.") # This will be printed to console
# Usage
print("This line prints to console.")
with suppress_stdout_stderr():
print("This line will NOT be seen on the console.")
sys.stdout.write("Nor will this direct write.\n")
sys.stderr.write("Errors also won't be seen.\n")
# You can call functions that print here
# import warnings
# warnings.warn("This warning will also be suppressed from console.")
print("Still inside suppressed block.")
print("This line prints to console again after suppression.")
# Demonstrate with a function that prints
def noisy_function():
print("I am a noisy function!")
with suppress_stdout_stderr():
noisy_function()
print("Noisy function output was suppressed.")
Explanation: This advanced example demonstrates a clever use of @contextmanager
to temporarily suppress all output to the console (both stdout
and stderr
). The suppress_stdout_stderr
function redirects these streams to os.devnull
(a "black hole" for output) before yielding. After the with
block finishes (in the finally
block), the original streams are restored. This can be very useful for running "noisy" third-party libraries or legacy code in your application without cluttering the console, particularly in background processes or when generating reports.