Web Analytics

Context Managers

Intermediate ~30 min read

Context managers are one of Python's most elegant features - they ensure resources are properly acquired and released, even when errors occur. The with statement is the Pythonic way to handle files, database connections, locks, and any resource that needs cleanup. Understanding context managers will make your code safer, cleaner, and more professional!

The 'with' Statement

The with statement guarantees that cleanup code runs no matter what - even if an exception occurs. For files, this means they're always closed properly. Compare it to try/finally: the with statement is cleaner and less error-prone.

Output
Click Run to execute your code
Why Use Context Managers?
- Automatic cleanup (files closed, connections released)
- Exception-safe (cleanup runs even if errors occur)
- Cleaner code (no explicit try/finally blocks)
- Clear intent (setup and cleanup are paired together)
- Less bugs (can't forget to close resources)

Multiple Context Managers

You can use multiple context managers in a single with statement - perfect for copying files, processing input to output, or managing multiple resources together. All resources are cleaned up in reverse order of acquisition, ensuring proper shutdown.

Output
Click Run to execute your code
Multiple Files Syntax (Python 3.10+):
with (
    open("input.txt") as infile,
    open("output.txt", "w") as outfile,
    open("log.txt", "a") as logfile
):
    # All three files available here
For older Python, use: with open("a") as f1, open("b") as f2:

Creating Custom Context Managers

Any class can become a context manager by implementing __enter__ and __exit__ methods. The __enter__ method sets up the resource and returns what gets bound to the as variable. The __exit__ method handles cleanup and can optionally suppress exceptions.

Output
Click Run to execute your code
Exception Handling in __exit__: The __exit__ method receives exception info as arguments. Return True to suppress the exception (usually not recommended), or False to let it propagate. The cleanup code should run regardless!

The contextlib Module

Writing a class with __enter__ and __exit__ can be verbose. The contextlib module provides shortcuts: the @contextmanager decorator turns a generator function into a context manager, and utilities like suppress() and closing() handle common patterns.

Output
Click Run to execute your code
contextlib Utilities:
@contextmanager - Create context manager from generator
closing(obj) - Call obj.close() on exit
suppress(*exceptions) - Ignore specified exceptions
redirect_stdout(f) - Redirect print() to file
nullcontext() - Do-nothing context manager
ExitStack() - Manage dynamic number of context managers

Common Mistakes

1. Using file object after 'with' block

# Wrong - file is closed!
with open("data.txt", "r") as f:
    pass

content = f.read()  # ValueError: I/O on closed file

# Correct - read inside the block
with open("data.txt", "r") as f:
    content = f.read()

# Or store what you need
with open("data.txt", "r") as f:
    lines = f.readlines()  # Now 'lines' persists

2. Forgetting 'as' when you need the resource

# Wrong - no way to access the file!
with open("data.txt", "r"):
    content = ???  # Can't read without variable

# Correct - bind to variable
with open("data.txt", "r") as f:
    content = f.read()

3. Not yielding in @contextmanager

from contextlib import contextmanager

# Wrong - forgot yield!
@contextmanager
def broken_manager():
    print("Setup")
    # Missing yield!
    print("Cleanup")

# Correct - must yield
@contextmanager
def working_manager():
    print("Setup")
    yield  # Control returns here
    print("Cleanup")

4. Swallowing all exceptions in __exit__

# Wrong - hides all errors!
def __exit__(self, exc_type, exc_val, exc_tb):
    self.cleanup()
    return True  # Suppresses ALL exceptions!

# Correct - only suppress specific exceptions
def __exit__(self, exc_type, exc_val, exc_tb):
    self.cleanup()
    if exc_type is SomeExpectedException:
        return True  # Only suppress this one
    return False  # Let others propagate

5. Cleanup not in finally block for @contextmanager

from contextlib import contextmanager

# Wrong - cleanup skipped on exception!
@contextmanager
def unsafe():
    resource = acquire()
    yield resource
    release(resource)  # Skipped if exception!

# Correct - use try/finally
@contextmanager
def safe():
    resource = acquire()
    try:
        yield resource
    finally:
        release(resource)  # Always runs

Exercise: Database Connection Manager

Task: Create a context manager that simulates database connection handling.

Requirements:

  • Create a class-based context manager for database connections
  • Print messages when connecting and disconnecting
  • Include a query() method to simulate database queries
  • Ensure the connection closes even if an error occurs
Output
Click Run to execute your code
Show Solution
class DatabaseConnection:
    """Simulated database connection context manager."""

    def __init__(self, database_name):
        self.database_name = database_name
        self.connected = False

    def __enter__(self):
        print(f"Connecting to database: {self.database_name}")
        self.connected = True
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Disconnecting from: {self.database_name}")
        self.connected = False
        if exc_type:
            print(f"Error occurred: {exc_val}")
        return False  # Don't suppress exceptions

    def query(self, sql):
        if not self.connected:
            raise RuntimeError("Not connected!")
        print(f"Executing: {sql}")
        return f"Results for: {sql}"


# Test normal operation
print("=== Normal Operation ===")
with DatabaseConnection("myapp.db") as db:
    result = db.query("SELECT * FROM users")
    print(result)
print()

# Test with exception
print("=== With Exception ===")
try:
    with DatabaseConnection("myapp.db") as db:
        db.query("SELECT * FROM users")
        raise ValueError("Simulated error!")
except ValueError:
    print("Exception caught outside context")
print()

# Verify connection is closed after exception
print(f"Connection state: {'connected' if db.connected else 'disconnected'}")

Summary

  • with statement: with open("f") as f: - auto cleanup
  • Multiple resources: with open("a") as f1, open("b") as f2:
  • __enter__: Setup code, return value bound to as variable
  • __exit__: Cleanup code, receives exception info, return True to suppress
  • @contextmanager: Create from generator with yield
  • closing(): Call .close() automatically
  • suppress(): Ignore specific exceptions
  • Always cleanup: Use try/finally in @contextmanager
  • Exception safety: Cleanup runs even if errors occur
  • Best practice: Use context managers for all resources

What's Next?

Now that you understand context managers, let's explore working with paths using Python's os.path and the modern pathlib module. These tools make it easy to navigate directories, join paths, check file existence, and write cross-platform code!