Skip to content

contextlib Module Complexity

The contextlib module provides utilities for working with context managers and the with statement, enabling resource management and cleanup.

Complexity Reference

Operation Time Space Notes
Context manager entry O(1) O(1) __enter__() call
Context manager exit O(1) O(1) __exit__() call
@contextmanager O(1) O(1) Decorator application
ExitStack add O(1) O(1) Register callback
ExitStack exit all O(n) O(1) LIFO order; n = registered callbacks
contextmanager yield O(1) O(1) Generator yield point

Context Managers Basics

Custom Context Manager Class

class FileManager:
    """Custom context manager - O(1) per operation"""

    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None

    # Entry - O(1)
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    # Exit - O(1)
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        return False  # Propagate exceptions

# Use context manager - O(1) per operation
with FileManager("test.txt", "w") as f:
    f.write("Hello")  # File closed automatically

print("File is closed")  # Guaranteed cleanup

Protocol: Context Manager

class Resource:
    """Implement context manager protocol"""

    def __init__(self, name):
        self.name = name
        self.is_open = False

    def __enter__(self):
        """Called on 'with' statement - O(1)"""
        print(f"Acquiring {self.name}")
        self.is_open = True
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Called on exit - O(1)"""
        print(f"Releasing {self.name}")
        self.is_open = False

        # Handle exceptions if needed
        if exc_type is not None:
            print(f"Exception occurred: {exc_type.__name__}")

        return False  # Don't suppress exceptions

# Usage - guaranteed cleanup
with Resource("database") as resource:
    print(f"Using {resource.name}")

# Output:
# Acquiring database
# Using database
# Releasing database

contextmanager Decorator

Simple Generator Context Manager

from contextlib import contextmanager

# Define with decorator - O(1)
@contextmanager
def timer(name):
    """Simple timer context manager"""
    import time

    # Setup - O(1)
    print(f"Starting {name}")
    start = time.time()

    try:
        # Yield control - O(1)
        yield
    finally:
        # Cleanup - O(1)
        elapsed = time.time() - start
        print(f"Finished {name}: {elapsed:.3f}s")

# Use - O(1) per operation
with timer("computation"):
    total = sum(range(1000000))

# Output:
# Starting computation
# Finished computation: 0.001s

Context Manager with Return Value

from contextlib import contextmanager

@contextmanager
def database_connection(db_url):
    """Context manager yielding resource"""

    # Setup - O(1)
    print(f"Connecting to {db_url}")
    connection = f"Connection to {db_url}"

    try:
        # Yield resource - O(1)
        yield connection
    finally:
        # Cleanup - O(1)
        print(f"Closing connection")

# Use with resource - O(1)
with database_connection("postgresql://localhost") as conn:
    print(f"Using {conn}")
    # query_result = conn.execute("SELECT * FROM users")

# Output:
# Connecting to postgresql://localhost
# Using Connection to postgresql://localhost
# Closing connection

Context Manager with Exception Handling

from contextlib import contextmanager

@contextmanager
def error_handler(error_message):
    """Catch exceptions in context"""

    try:
        # Yield control - O(1)
        yield
    except Exception as e:
        # Handle exception - O(1)
        print(f"{error_message}: {type(e).__name__}")

# Use with error handling - O(1)
with error_handler("Operation failed"):
    result = 1 / 0  # Will be caught

print("Execution continues")

# Output:
# Operation failed: ZeroDivisionError
# Execution continues

ExitStack - Multiple Context Managers

Register Multiple Contexts

from contextlib import ExitStack

# ExitStack - O(1) per add
with ExitStack() as stack:
    # Add file contexts - O(1) each
    f1 = stack.enter_context(open("file1.txt", "w"))
    f2 = stack.enter_context(open("file2.txt", "w"))
    f3 = stack.enter_context(open("file3.txt", "w"))

    # Use all files - O(1) per operation
    f1.write("File 1 content")
    f2.write("File 2 content")
    f3.write("File 3 content")

# All files closed automatically - O(n) for n files
print("All files closed")

Conditional Context Management

from contextlib import ExitStack

def open_optional_file(filename=None, mode="r"):
    """Open file only if filename provided"""

    stack = ExitStack()
    files = []

    if filename:
        # Conditionally add context - O(1)
        f = stack.enter_context(open(filename, mode))
        files.append(f)

    # Return both stack and files for cleanup
    return stack, files

# Use - O(1) per operation
stack, files = open_optional_file("data.txt")
with stack:
    if files:
        content = files[0].read()
        print(content)

# File closed on exit - O(1)

Register Callbacks

from contextlib import ExitStack

with ExitStack() as stack:
    # Register callback - O(1) per callback
    stack.callback(print, "Cleanup 3")
    stack.callback(print, "Cleanup 2")
    stack.callback(print, "Cleanup 1")

    print("Main context")

# Output (LIFO order):
# Main context
# Cleanup 1
# Cleanup 2
# Cleanup 3

Suppress Context Manager

Suppress Exceptions

from contextlib import suppress

# Suppress specific exception - O(1)
with suppress(FileNotFoundError):
    with open("nonexistent.txt") as f:
        content = f.read()

# No exception raised
print("Execution continues despite FileNotFoundError")

# Multiple exceptions - O(1)
with suppress(ValueError, KeyError, AttributeError):
    value = int("not a number")  # Suppressed

Redirect Context Managers

Redirect Output

from contextlib import redirect_stdout, redirect_stderr
import io

# Capture stdout - O(1) setup
output = io.StringIO()
with redirect_stdout(output):
    print("This goes to StringIO")
    print("Not to console")

captured = output.getvalue()
print(f"Captured: {captured}")

# Capture stderr - O(1) setup
errors = io.StringIO()
with redirect_stderr(errors):
    import sys
    sys.stderr.write("Error message")

print(f"Errors: {errors.getvalue()}")

nullcontext - No-op Context

Placeholder Context Manager

from contextlib import nullcontext

# nullcontext - O(1), does nothing
with nullcontext() as nothing:
    print("Nothing is:", nothing)

# Return value
with nullcontext("default value") as value:
    print("Value is:", value)

# Conditional context manager
def optional_context(condition, context_manager):
    """Use context manager only if condition is true"""
    if condition:
        return context_manager
    return nullcontext()

# Usage
config = {'debug': True}

with optional_context(config.get('debug'), suppress(RuntimeError)):
    if config['debug']:
        raise RuntimeError("Debug error")

Asyncio Context Managers

Async Context Manager

from contextlib import asynccontextmanager
import asyncio

# Define async context manager - O(1)
@asynccontextmanager
async def async_timer(name):
    """Async context manager"""

    # Setup - O(1)
    print(f"Starting {name}")
    import time
    start = time.time()

    try:
        # Yield control - O(1)
        yield
    finally:
        # Cleanup - O(1)
        elapsed = time.time() - start
        print(f"Finished {name}: {elapsed:.3f}s")

# Use async context - O(?) based on async operations
async def main():
    async with async_timer("async operation"):
        await asyncio.sleep(0.1)

# asyncio.run(main())

Common Patterns

Resource Pool Management

from contextlib import contextmanager

class ConnectionPool:
    """Simple connection pool"""

    def __init__(self, size=5):
        self.pool = [f"Connection-{i}" for i in range(size)]
        self.available = set(self.pool)

    @contextmanager
    def acquire(self):
        """Acquire connection from pool - O(1) amortized"""

        # Get connection - O(1)
        conn = self.available.pop()

        try:
            yield conn
        finally:
            # Return to pool - O(1)
            self.available.add(conn)

# Use pool - O(1) per operation
pool = ConnectionPool(3)

with pool.acquire() as conn:
    print(f"Using {conn}")
    # Simulate work

# Connection returned - O(1)
print(f"Available: {len(pool.available)}")

Temporary Changes

from contextlib import contextmanager
import sys

@contextmanager
def temporary_setting(obj, name, value):
    """Temporarily change object attribute - O(1)"""

    # Save original - O(1)
    old_value = getattr(obj, name)
    setattr(obj, name, value)

    try:
        yield
    finally:
        # Restore original - O(1)
        setattr(obj, name, old_value)

class Config:
    debug = False

# Use - O(1)
with temporary_setting(Config, 'debug', True):
    print(f"Debug: {Config.debug}")  # True

print(f"Debug: {Config.debug}")  # False

Performance Notes

Time Complexity

  • Context entry/exit: O(1) for simple cases
  • ExitStack operations: O(1) per add, O(n) to exit all
  • Multiple contexts: O(n) total where n = number of contexts

Space Complexity

  • Context manager: O(1) for simple cases
  • ExitStack: O(n) for n registered callbacks/contexts
  • Output redirection: O(n) for captured output

Best Practices

  • Use context managers for resource cleanup
  • Use ExitStack for multiple resources
  • Use suppresses sparingly for expected errors
  • Use try/finally for custom cleanup