✦ For everyone, free.

Practical knowledge for real and everyday life

Home

Context Management in Python

Context Management in Python ensures resources are properly acquired and released, using context managers to handle setup and teardown in a clean, readable way.

Context management in Python is the structured establishment and finalization of temporary execution contexts around a block of code. It allows setup, resource coordination, state restoration, cleanup, and optional exception handling to be expressed through synchronous or asynchronous context-manager protocols.


Foundations of Context Management in Python

A context manager is an object that participates in entry and exit protocols, which surround the execution of a managed suite (a block of code). The context manager object itself is distinct from any resource or value it may provide during execution.

Context management involves:

  • Setup: Preparing the environment or resource before the suite runs.
  • Entry: Entering the context, typically through an __enter__ method.
  • Managed execution: Running the suite of code under the managed context.
  • Exit: Leaving the context, typically through an __exit__ method.
  • Cleanup: Releasing or restoring any altered state or resource.
  • Optional response to exceptional completion: Handling exceptions raised during the suite.

Setup and cleanup responsibilities should remain paired to guarantee reliable resource management.

Representative uses of context managers include:

  • Managing files (open and close)
  • Acquiring and releasing locks (thread synchronization)
  • Transactions (database commit or rollback)
  • Temporary state changes (e.g., switching locale or configuration)
  • Redirecting standard output or error
  • Managing numerical precision contexts
  • Other scoped behaviors affecting execution environment

Not every context manager owns an external resource; some manage state or logical boundaries.

ConceptPrincipal Responsibility
Context ManagerProvides entry and exit methods to establish and finalize context
with StatementControls flow: evaluates context expression, enters manager, executes suite, ensures exit is called
__enter__ MethodPerforms setup and returns an optional value bound by as
__exit__ MethodPerforms cleanup, handles optional exception info, optionally suppresses exceptions
Exception SuppressionDecided by __exit__ return value (True suppresses exception, False propagates)
Class-based Context ManagerImplements __enter__ and __exit__ methods
Generator-based Context ManagerUses contextlib.contextmanager decorator with generator yielding managed value
ExitStackDynamically manages a stack of exit callbacks for flexible context management
Asynchronous Context ManagerImplements asynchronous entry/exit via __aenter__ and __aexit__
__aenter__ MethodPerforms asynchronous setup and returns awaited value for async with
__aexit__ MethodPerforms asynchronous cleanup, receives exception info, optionally suppresses exceptions

Synchronous Context Management Evaluate Context Expression Call __enter__ Optional Binding Execute Managed Suite Call __exit__ Exception? Handle Exception in __exit__ Suppress or Propagate Continue or Propagate Exception Asynchronous Context Management Evaluate Context Expression Await __aenter__ Optional Binding Execute Managed Suite Await __aexit__ Exception? Handle Exception in __aexit__ Suppress or Propagate Continue or Propagate Exception

Python with Statement

The with statement controls execution flow by:

  1. Evaluating a context expression.
  2. Entering the resulting context manager by calling its __enter__ method.
  3. Optionally binding the value returned by __enter__ to a target after as.
  4. Executing the managed suite (block of code).
  5. Invoking the context manager’s __exit__ method when the suite finishes, regardless of how it exits.

The context expression produces an object that must implement the context-manager protocol. The value bound by the as clause is whatever __enter__ returns and need not be the context manager object itself.

Example using a file context manager:

with open('example.txt', 'w') as file:
    file.write('Hello, context management!\n')

# After the block, the file is automatically closed.

In this example:

  • The context expression is open('example.txt', 'w').
  • The context manager is the file object returned by open.
  • The value bound to file is the same file object.
  • Inside the suite, text is written to the file.
  • On exit, the file’s __exit__ closes the file, ensuring no resource leak.

Multiple context managers can be combined in a single with statement:

with open('input.txt') as infile, open('output.txt', 'w') as outfile:
    for line in infile:
        outfile.write(line.upper())

Entry order is left to right (infile then outfile), while exit order is reversed (outfile then infile).

A tracing example demonstrating entry and exit order with two simple context managers:

class TraceCM:
    def __init__(self, name):
        self.name = name
    def __enter__(self):
        print(f'Entering {self.name}')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f'Exiting {self.name}')
        return False

with TraceCM('First'), TraceCM('Second'):
    print('Inside with block')

Output:

Entering First
Entering Second
Inside with block
Exiting Second
Exiting First

Nested with statements are conceptually similar to multiple context managers in one statement but allow explicit control over dependencies:

with TraceCM('Outer'):
    with TraceCM('Inner'):
        print('Nested with blocks')

Leaving a with suite by any means—normal completion, return, break, continue, or exception propagation—always invokes the context manager’s exit protocol.

Example demonstrating early return with guaranteed exit:

class DemoCM:
    def __enter__(self):
        print('Entered')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Exited')
        return False

def func():
    with DemoCM():
        print('Before return')
        return 'Returned early'
        print('This line never executes')

result = func()
print(result)

Output:

Entered
Before return
Exited
Returned early

The with statement conceptually corresponds to disciplined setup and cleanup using try and finally, but it is not merely syntactic sugar for one fixed try/finally pattern. Instead, it delegates responsibility to the context manager for setup and cleanup decisions.

Comparison of explicit setup/cleanup vs. with:

# Explicit try/finally
resource = acquire_resource()
try:
    use(resource)
finally:
    release_resource(resource)

# With statement
with resource_manager():
    use(resource)

The context manager encapsulates setup and cleanup logic, promoting separation of concerns and reuse.


Exception Handling by Python Context Managers

A synchronous context manager’s __exit__ method receives three arguments describing the managed suite’s exceptional completion or None-equivalent values if completion was normal:

  • exc_type: the exception class if an exception occurred, else None.
  • exc_val: the exception instance if an exception occurred, else None.
  • exc_tb: the traceback object if an exception occurred, else None.

Example class-based tracing context manager printing exception info:

class ExceptionTracer:
    def __enter__(self):
        print('Entering context')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print(f'Exception type: {exc_type.__name__}')
            print(f'Exception value: {exc_val}')
            print(f'Traceback: {exc_tb}')
        else:
            print('Exited normally without exception')
        return False  # Do not suppress exceptions

with ExceptionTracer():
    print('Inside context')
    # No exception here

Exception suppression is controlled by the return value of __exit__:

  • Returning True suppresses the exception, preventing propagation.
  • Returning False (or None) propagates the exception.

Two contrasting context managers:

class PropagateCM:
    def __enter__(self): return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Cleaning up, propagating exception if any')
        return False  # Propagate exception

class SuppressValueErrorCM:
    def __enter__(self): return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is ValueError:
            print('Suppressing ValueError')
            return True  # Suppress only ValueError
        print('Not suppressing exception')
        return False

# Usage:
with PropagateCM():
    raise ValueError('This will propagate')

with SuppressValueErrorCM():
    raise ValueError('This will be suppressed')

Unconditional or broad exception suppression can hide unrelated defects and complicate debugging. Suppression should be used only when semantically justified.

If an exception is raised during context-manager entry (__enter__), the managed suite does not execute.

If an exception is raised during context-manager exit (__exit__), it can replace or chain with any existing exception propagated from the managed suite.

Examples:

class FailOnEnterCM:
    def __enter__(self):
        print('Failing on enter')
        raise RuntimeError('Entry failure')
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Exit called')

try:
    with FailOnEnterCM():
        print('Won’t run')
except RuntimeError as e:
    print(f'Caught: {e}')

class FailOnExitCM:
    def __enter__(self):
        print('Entering successfully')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Failing on exit')
        raise RuntimeError('Exit failure')

try:
    with FailOnExitCM():
        print('Inside managed suite')
except RuntimeError as e:
    print(f'Caught: {e}')

Exception translation by a context manager should be meaningful and preserve causal information via exception chaining (e.g., raise NewError() from original_error).


Custom Context Managers in Python

Custom context managers implement scoped setup and finalization behavior for managing temporary state, resources, synchronization, or domain-specific execution contexts.

Class-Based Context Managers in Python

A class-based synchronous context manager implements:

  • __enter__(self) — performs context-entry work, returning a value optionally bound by the as clause. This value may be self, another object, or any suitable value.
  • __exit__(self, exc_type, exc_val, exc_tb) — performs context-finalization work and optionally determines whether a propagating exception is suppressed by returning True or False.

Example: A context manager that temporarily changes an object's state and restores it on exit:

class TempState:
    def __init__(self, obj, attr, new_value):
        self.obj = obj
        self.attr = attr
        self.new_value = new_value
        self.old_value = None

    def __enter__(self):
        self.old_value = getattr(self.obj, self.attr)
        setattr(self.obj, self.attr, self.new_value)
        return self  # Could return something else as needed

    def __exit__(self, exc_type, exc_val, exc_tb):
        setattr(self.obj, self.attr, self.old_value)
        # Do not suppress exceptions
        return False

# Usage example
class Config:
    mode = 'normal'

config = Config()
print(f'Before: {config.mode}')  # normal

with TempState(config, 'mode', 'temporary'):
    print(f'During: {config.mode}')  # temporary

print(f'After: {config.mode}')  # normal

Reusability means the same context manager instance can be used in multiple separate with statements safely, while reentrancy means that the same instance can be entered multiple times concurrently or nested, which is often unsafe or unsupported.

Example illustrating reuse vs. reentry difference:

class SimpleCM:
    def __init__(self):
        self.active = False

    def __enter__(self):
        if self.active:
            raise RuntimeError('Already active')
        print('Entering')
        self.active = True
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Exiting')
        self.active = False
        return False

cm = SimpleCM()

# Reuse: different with statements
with cm:
    print('First block')

with cm:
    print('Second block')

# Reentry: nested usage (raises)
try:
    with cm:
        with cm:
            print('Nested block')
except RuntimeError as e:
    print(f'Error: {e}')

Generator-Based Context Managers in Python

Using contextlib.contextmanager, context managers can be created from generator functions:

  • Code before the yield performs setup.
  • The yielded value becomes the optional as value.
  • Code after the yield performs cleanup, executed regardless of normal or exceptional completion.

Example:

from contextlib import contextmanager

@contextmanager
def temp_state(obj, attr, new_value):
    old_value = getattr(obj, attr)
    setattr(obj, attr, new_value)
    try:
        yield
    finally:
        setattr(obj, attr, old_value)

# Usage
class Config:
    mode = 'normal'

config = Config()
print(f'Before: {config.mode}')

with temp_state(config, 'mode', 'temporary'):
    print(f'During: {config.mode}')

print(f'After: {config.mode}')

Exceptions raised in the managed suite are injected at the suspended yield point. Generator-based context managers must propagate exceptions they do not handle deliberately.

Example handling one exception type:

@contextmanager
def suppress_value_error():
    try:
        yield
    except ValueError:
        print('ValueError suppressed')

with suppress_value_error():
    raise ValueError('Test')  # suppressed

with suppress_value_error():
    raise KeyError('Test')  # propagated

Comparison between class-based and generator-based context managers:

FeatureClass-BasedGenerator-Based
DefinitionClass with __enter__ and __exit__Generator function decorated with @contextmanager
Entry LogicDefined in __enter__Code before yield
Managed ValueReturn value of __enter__Value yielded
Exit LogicDefined in __exit__Code after yield
Stored StateInstance attributesLocal variables in generator
ReusePossible with multiple with statementsUsually new generator per use
ComplexitySupports complex state and methodsSimpler for paired setup/cleanup
Exception HandlingExplicit in __exit__Exception injected at yield

Standard utilities like closing, suppress, and nullcontext illustrate reusable context-manager patterns without exhaustive cataloging.

Examples:

from contextlib import closing, suppress

# closing: ensures close() called on non-context-manager resource
class Resource:
    def close(self):
        print('Resource closed')

with closing(Resource()) as res:
    print('Using resource')

# suppress: suppresses specified exceptions
with suppress(FileNotFoundError):
    open('nonexistent.file')

print('After suppress block')

Dynamic Context Management in Python

Dynamic context management handles acquiring and releasing a runtime-determined number or arrangement of context managers when static lexical nesting is insufficient.

contextlib.ExitStack maintains a last-in-first-out stack of exit callbacks and entered context managers.

enter_context enters another context manager immediately and registers its exit callback with the stack, so cleanup occurs in reverse order.

Example opening a runtime-determined collection of files:

from contextlib import ExitStack

filenames = ['file1.txt', 'file2.txt', 'file3.txt']

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    for f in files:
        print(f.read())

# Files are closed in reverse order after the block

Cleanup callbacks can be registered as plain callables or full context-manager exit methods that accept exception info.

Example combining an entered context manager and explicit cleanup callback:

def cleanup():
    print('Custom cleanup callback')

with ExitStack() as stack:
    stack.callback(cleanup)
    with stack.enter_context(open('example.txt')) as f:
        print(f.read())

Conditional acquisition example:

with ExitStack() as stack:
    if condition:
        resource = stack.enter_context(open('file1.txt'))
    else:
        resource = stack.enter_context(open('file2.txt'))
    # Use resource safely

# Cleanup is centralized and in reverse order regardless of path

ExitStack supports transferring registered callbacks to another stack, distinguishing transferring cleanup responsibility from immediate execution.

Example handling partial acquisition failure with proper cleanup:

from contextlib import ExitStack

resources_to_open = ['file1.txt', 'file2.txt', 'missing.txt']

try:
    with ExitStack() as stack:
        files = []
        for fname in resources_to_open:
            f = stack.enter_context(open(fname))  # May raise
            files.append(f)
        # Use files...
except FileNotFoundError as e:
    print(f'Acquisition failed: {e}')
# Successfully opened files are cleaned up automatically in reverse order

Asynchronous Context Management in Python

Asynchronous context management provides scoped setup and finalization where context entry or exit can require asynchronous suspension.

The async with Statement in Python

async with is the asynchronous counterpart of synchronous context management, using awaited asynchronous entry and exit protocols.

An asynchronous context manager implements:

  • __aenter__(self) — returns an awaitable which, when awaited, performs asynchronous setup and returns an optional value for binding.
  • __aexit__(self, exc_type, exc_val, exc_tb) — returns an awaitable which, when awaited, performs asynchronous cleanup and optionally suppresses exceptions.

Example asynchronous context manager:

import asyncio

class AsyncDemoCM:
    async def __aenter__(self):
        print('Async enter')
        await asyncio.sleep(0.1)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print('Async exit')
        await asyncio.sleep(0.1)
        return False

async def main():
    async with AsyncDemoCM() as cm:
        print('Inside async with')

import asyncio
asyncio.run(main())

The optional value bound after as is the awaited result of __aenter__, distinguished from the asynchronous context manager instance itself.

Exceptions raised in the managed suite are passed to __aexit__ as with synchronous context managers. The awaited result of __aexit__ determines if exception suppression occurs.

Example where an exception propagates after asynchronous cleanup:

class AsyncSuppressCM:
    async def __aenter__(self):
        print('Enter async context')
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print(f'Exit async context (exception: {exc_type})')
        await asyncio.sleep(0.1)
        return False  # Do not suppress

async def raise_exception():
    async with AsyncSuppressCM():
        raise RuntimeError('Error inside async with')

asyncio.run(raise_exception())

Custom Asynchronous Context Managers in Python

Asynchronous context managers are appropriate when acquisition or cleanup requires suspension (e.g., network connections, async locks).

Generator-based asynchronous context managers use contextlib.asynccontextmanager, combining async setup before yield and awaited or synchronous cleanup after.

Example:

from contextlib import asynccontextmanager
import asyncio

@asynccontextmanager
async def async_temp_state(obj, attr, new_value):
    old_value = getattr(obj, attr)
    setattr(obj, attr, new_value)
    try:
        yield
    finally:
        await asyncio.sleep(0.1)
        setattr(obj, attr, old_value)

class Config:
    mode = 'normal'

async def main():
    config = Config()
    print(f'Before: {config.mode}')
    async with async_temp_state(config, 'mode', 'async_temporary'):
        print(f'During: {config.mode}')
    print(f'After: {config.mode}')

asyncio.run(main())

Dynamic asynchronous context management is supported by AsyncExitStack, which manages multiple asynchronous or synchronous context managers with coordinated cleanup.

Example:

from contextlib import AsyncExitStack
import asyncio

class AsyncResource:
    async def __aenter__(self):
        print('AsyncResource enter')
        await asyncio.sleep(0.1)
        return self
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print('AsyncResource exit')
        await asyncio.sleep(0.1)

async def main():
    async with AsyncExitStack() as stack:
        res1 = await stack.enter_async_context(AsyncResource())
        res2 = await stack.enter_async_context(AsyncResource())
        print('Inside async with multiple resources')

asyncio.run(main())
Context Manager TypeEntry ProtocolExit ProtocolSupports SuspensionRepresentative Use
Class-based (sync)__enter__()__exit__()NoComplex resource/state management
Generator-based (sync)pre-yield codepost-yield codeNoSimple paired setup/cleanup
Class-based (async)__aenter__()__aexit__()YesAsync resource/state management
Generator-based (async)pre-yield asyncpost-yield asyncYesAsync paired setup/cleanup
ExitStack (sync dynamic)enter_context()stack exit callsNoRuntime-determined context sets
AsyncExitStack (async dynamic)enter_async_context()stack async exit callsYesRuntime-determined async contexts

Asynchronous context management addresses asynchronous setup and cleanup needs specifically and should not be used solely because surrounding code is declared with async def.


Solved Context Management Exercises in Python

Exercise 1: Class-Based Context Manager with State Change and Exception Suppression

class StateSwitcher:
    def __init__(self, obj, attr, new_value):
        self.obj = obj
        self.attr = attr
        self.new_value = new_value
        self.old_value = None

    def __enter__(self):
        # Validate entry condition
        if not hasattr(self.obj, self.attr):
            raise AttributeError(f'{self.obj} lacks attribute {self.attr}')
        self.old_value = getattr(self.obj, self.attr)
        setattr(self.obj, self.attr, self.new_value)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Always restore state
        setattr(self.obj, self.attr, self.old_value)

        # Suppress only ValueError exceptions
        if exc_type is ValueError:
            print('ValueError suppressed')
            return True
        # Propagate others
        return False

# Usage demonstration
class Application:
    mode = 'default'

app = Application()
print(f'Initial mode: {app.mode}')

try:
    with StateSwitcher(app, 'mode', 'temporary'):
        print(f'Inside context: mode = {app.mode}')
        raise ValueError('Test ValueError')
except Exception as e:
    print(f'Caught exception: {e}')

print(f'After context: mode = {app.mode}')

Step-by-step explanation:

  • The manager is created with the target object, attribute, and new temporary value.
  • __enter__ checks if the attribute exists and saves the old value.
  • The attribute is set to the new value, which is bound to the target variable if as is used.
  • Inside the managed suite, the attribute temporarily holds the new value.
  • On normal or exceptional exit, __exit__ restores the original attribute value.
  • If a ValueError is raised, it is suppressed; other exceptions propagate.
  • Final state after the block reflects restoration regardless of completion.

Exercise 2: Generator-Based Context Manager with ExitStack for Dynamic Resource Management

from contextlib import contextmanager, ExitStack

@contextmanager
def managed_resource(name):
    print(f'Acquiring {name}')
    try:
        yield name
    finally:
        print(f'Releasing {name}')

def dynamic_resources(names):
    with ExitStack() as stack:
        resources = []
        for name in names:
            # Simulate failure on "bad_resource"
            if name == 'bad_resource':
                raise RuntimeError('Failed to acquire resource')
            res = stack.enter_context(managed_resource(name))
            resources.append(res)
        print(f'Using resources: {resources}')

# Testing with partial failure
try:
    dynamic_resources(['res1', 'res2', 'bad_resource', 'res3'])
except RuntimeError as e:
    print(f'Caught: {e}')

Explanation:

  • managed_resource is a generator-based context manager simulating acquisition and release.
  • ExitStack dynamically manages a list of resources determined at runtime.
  • If acquisition fails partway, previously acquired resources are released in reverse order.
  • The exception propagates after cleanup.
  • This pattern centralizes cleanup logic despite dynamic acquisition.

Exercise 3: Asynchronous Generator-Based Context Manager with AsyncExitStack and Exception Handling

from contextlib import asynccontextmanager, AsyncExitStack
import asyncio

@asynccontextmanager
async def async_resource(name):
    print(f'Async acquiring {name}')
    await asyncio.sleep(0.1)
    try:
        yield name
    finally:
        print(f'Async releasing {name}')
        await asyncio.sleep(0.1)

async def async_dynamic_resources(names):
    async with AsyncExitStack() as stack:
        resources = []
        for name in names:
            if name == 'bad_async_resource':
                raise RuntimeError('Async acquisition failure')
            res = await stack.enter_async_context(async_resource(name))
            resources.append(res)
        print(f'Using async resources: {resources}')
        # Simulate exception inside managed suite
        raise ValueError('Error during async usage')

async def main():
    try:
        await async_dynamic_resources(['ares1', 'ares2', 'bad_async_resource', 'ares3'])
    except Exception as e:
        print(f'Caught in main: {e}')

asyncio.run(main())

Explanation:

  • async_resource is an async generator-based context manager simulating asynchronous setup and cleanup.
  • AsyncExitStack manages multiple asynchronous resources dynamically.
  • Partial acquisition failure triggers cleanup of successfully acquired resources.
  • An exception raised inside the managed suite propagates after awaited cleanup.
  • Awaited acquisition, suspension, dynamic registration, and reverse-order asynchronous cleanup are demonstrated.