The with statement guarantees that setup and teardown code runs, even if an exception occurs in The block body. It is the primary mechanism for resource management in Python.
with open ( " data.txt " ) as f:
# Sequential — both opened, both closed
with open ( " input.txt " ) as infile, open ( " output.txt " , " w " ) as outfile:
outfile.write(infile.read())
# Nested — same as above, different syntax
with open ( " input.txt " ) as infile:
with open ( " output.txt " , " w " ) as outfile:
outfile.write(infile.read())
# Parenthesized form (Python 3.10+)
open ( " input.txt " ) as infile,
open ( " output.txt " , " w " ) as outfile,
outfile.write(infile.read())
If the second `with` statement fails (e.g., `open("output.txt", "w")` raises
`PermissionError`), the first resource (`input.txt`) is still properly closed. This is a key Advantage over manual try/finally.The context manager protocol requires two methods:
def __init__ ( self , name ):
print ( f "Acquiring { self .name } " )
self ._resource = f "resource_ { self .name } "
return self ._resource # This is bound to the "as' variable
def __exit__ ( self , exc_type , exc_val , exc_tb ):
print ( f "Releasing { self .name } " )
print ( f "Exception in block: { exc_type. __name__} : { exc_val } " )
return False # Do not suppress exceptions
# Return True to suppress the exception
with Resource( " db_connection " ) as r:
# Acquiring db_connection
# Using: resource_db_connection
# Releasing db_connection
Parameter Description exc_typeException class, or None if no exception exc_valException instance, or None exc_tbTraceback object, or None
All three are None when the block exits normally. When an exception occurs, they contain the Exception details.
def __exit__ ( self , exc_type , exc_val , exc_tb ):
if exc_type is not None and issubclass (exc_type, ValueError ):
print ( f "Suppressed: { exc_val } " )
return True # Suppress ValueError
return False # Re-raise everything else
raise ValueError ( " oops " ) # Suppressed
raise TypeError ( " oops " ) # Re-raised
The contextlib module provides utilities for creating and working with context managers.
The @contextmanager decorator lets you create context managers using generator functions:
from contextlib import contextmanager
def managed_resource ( name ):
print ( f "Setting up { name } " )
resource = f "resource_ { name } "
yield resource # Value bound to 'as' variable
print ( f "Tearing down { name } " )
# Cleanup always runs, even on exception
with managed_resource( " database " ) as r:
# Using resource_database
The generator must yield exactly once. The code before yield is the setup, and the code in the finally block (or after yield) is the teardown.
from contextlib import contextmanager
raise # Re-raise unless you want to suppress
with transaction( " mydb " ):
print ( " Executing queries " )
# If no exception: BEGIN -> queries -> COMMIT
# If exception: BEGIN -> queries -> ROLLBACK -> re-raise
closing() creates a context manager from any object with a close() method:
from contextlib import closing
with closing(urllib.request.urlopen( " https://httpbin.org/get " )) as response:
print ( f "Status: { response.status } " )
from contextlib import suppress
# Cleanly ignore specific exceptions
with suppress( FileNotFoundError ):
os.remove( " /tmp/stale.lock " )
with suppress( PermissionError , FileNotFoundError ):
os.makedirs( " /opt/app/logs " , exist_ok = True )
from contextlib import redirect_stdout
with redirect_stdout(buffer):
print ( " This goes to the buffer " )
print ( f "Python: { sys.version } " )
output = buffer.getvalue()
print ( f "Captured { len (output) } bytes" )
from contextlib import redirect_stderr
with redirect_stderr(buffer):
logging.basicConfig( stream = sys.stderr, level = logging. WARNING )
logging.warning( " This goes to the buffer " )
errors = buffer.getvalue()
open() is the most common context manager. It guarantees the file is closed even if an exception Occurs:
with open ( " config.yaml " ) as f:
config = yaml.safe_load(f)
with open ( " output.csv " , " w " , newline = "" , encoding = " utf-8 " ) as f:
writer = csv.DictWriter(f, fieldnames = [ " name " , " email " ])
with open ( " app.log " , " a " , encoding = " utf-8 " ) as f:
f.write( f "[ { datetime.now() } ] Event occurred \n " )
with open ( " data.bin " , " rb " ) as f:
def __init__ ( self , path ):
self ._fd = open ( self .path, " w " )
fcntl.flock( self ._fd, fcntl. LOCK_EX )
def __exit__ ( self , * args ):
fcntl.flock( self ._fd, fcntl. LOCK_UN )
with FileLock( " /tmp/app.lock " ):
# Only one process can be in this block at a time
perform_critical_operation()
# Lock released when exiting the block
All synchronization primitives that have acquire()/release() methods work as context managers:
rlock = threading.RLock()
semaphore = threading.Semaphore( 3 )
event = threading.Event()
pass # Limited to 3 concurrent
with event: # Not useful for events — use event.wait() instead
def query_database ( db_path ):
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
with conn.cursor() as cursor:
cursor.execute( " SELECT * FROM users WHERE active = 1 " )
return [ dict (row) for row in rows]
# conn commits automatically if no exception (via context manager)
# conn rolls back if exception occurs
from contextlib import contextmanager
def __init__ ( self , create_connection , pool_size = 5 ):
self ._create = create_connection
self ._lock = threading.Lock()
self ._pool_size = pool_size
def _get_connection ( self ):
def _return_connection ( self , conn ):
if len ( self ._pool) < self ._pool_size:
conn = self ._get_connection()
self ._return_connection(conn)
pool = ConnectionPool( lambda : sqlite3.connect( " :memory: " ), pool_size = 5 )
with pool.connection() as conn:
cursor = conn.execute( " SELECT 1 " )
# Connection returned to pool
def __init__ ( self , base_url , api_key ):
self .session = requests.Session()
self .session.headers.update({
" Authorization " : f "Bearer { api_key } " ,
" Content-Type " : " application/json " ,
def __exit__ ( self , * args ):
def get ( self , path , ** kwargs ):
return self .session.get( f " { self .base_url }{ path } " , ** kwargs)
def post ( self , path , ** kwargs ):
return self .session.post( f " { self .base_url }{ path } " , ** kwargs)
with APIClient( " https://api.example.com " , " sk-123 " ) as client:
response = client.get( " /users " )
response = client.post( " /users " , json = { " name " : " Alice " })
# Session closed automatically
# tempfile.TemporaryDirectory is a built-in context manager
with tempfile.TemporaryDirectory() as tmpdir:
print ( f "Temp dir: { tmpdir } " )
filepath = os.path.join(tmpdir, " data.txt " )
with open (filepath, " w " ) as f:
f.write( " temporary data " )
print (os.path.exists(filepath)) # True
# Directory and all contents deleted
print (os.path.exists(tmpdir)) # False
with tempfile.NamedTemporaryFile( mode = " w " , suffix = " .txt " , delete = False ) as f:
# delete=False keeps the file after closing
# Remember to clean up manually:
def __init__ ( self , name ):
async def __aenter__ ( self ):
print ( f "Async acquiring { self .name } " )
async def __aexit__ ( self , exc_type , exc_val , exc_tb ):
print ( f "Async releasing { self .name } " )
async with AsyncResource( " database " ) as r:
# Async acquiring database
# Async releasing database
async with AsyncResource( " db " ), AsyncResource( " cache " ):
# Both released in reverse order
asyncio.AsyncExitStack manages multiple async context managers dynamically:
from contextlib import asynccontextmanager
async def async_db_connection ( host ):
print ( f "Connecting to { host } " )
conn = f "connection_ { host } "
print ( f "Disconnecting from { host } " )
async with asyncio.AsyncExitStack() as stack:
conn1 = await stack.enter_async_context(async_db_connection( " db1 " ))
conn2 = await stack.enter_async_context(async_db_connection( " db2 " ))
conn3 = await stack.enter_async_context(async_db_connection( " db3 " ))
print ( f "Using: { conn1 } , { conn2 } , { conn3 } " )
# All three connections close when the block exits
from contextlib import asynccontextmanager
# Used with FastAPI/Starlette:
# app = FastAPI(lifespan=lifespan)
from contextlib import contextmanager
start = time.perf_counter()
elapsed = time.perf_counter() - start
print ( f "[ { label } ] { elapsed :.4f } s" )
with timer( " database query " ):
# [database query] 0.5002s
with timer( " file processing " ):
process_large_file( " data.csv " )
from contextlib import contextmanager
def retry ( max_attempts = 3 , base_delay = 1.0 , exceptions = ( Exception ,)):
for attempt in range (max_attempts):
return # Success — exit context manager
if attempt < max_attempts - 1 :
delay = base_delay * ( 2 ** attempt) + random.uniform( 0 , 0.5 )
with retry( max_attempts = 3 , exceptions = ( ConnectionError ,)):
response = requests.get( " https://api.example.com/data " )
from contextlib import contextmanager
def database_transaction ( connection ):
cursor = connection.cursor()
print ( " Transaction committed " )
print ( " Transaction rolled back " )
from contextlib import contextmanager
logger = logging.getLogger( __name__ )
def log_duration ( level = logging. INFO , message = " Operation " ):
logger.log(level, f " { message } : starting" )
start = time.perf_counter()
elapsed = time.perf_counter() - start
logger.log(level, f " { message } : failed after { elapsed :.3f } s — { e } " )
elapsed = time.perf_counter() - start
logger.log(level, f " { message } : completed in { elapsed :.3f } s" )
with log_duration(logging. INFO , " Data migration " ):
from contextlib import contextmanager
def elevated_privileges ( uid , gid ):
original_uid = os.getuid()
original_gid = os.getgid()
with elevated_privileges( 0 , 0 ):
# Running as root for this block only
bind_to_privileged_port( 80 )
ExitStack manages a dynamic number of context managers, which is useful when the number of Resources is determined at runtime:
from contextlib import ExitStack
def process_files ( paths ):
with ExitStack() as stack:
files = [stack.enter_context( open (p)) for p in paths]
# All files are guaranteed to close when the block exits
# Even if an exception occurs while opening a later file
print ( f " { f.name } : { len (content) } bytes" )
from contextlib import ExitStack
def write_output ( data , output_path = None ):
with ExitStack() as stack:
f = stack.enter_context( open (output_path, " w " ))
writer.write( f " { line } \n " )
from contextlib import ExitStack
with ExitStack() as stack:
db = stack.enter_context(DatabaseConnection( " postgres://... " ))
cache = stack.enter_context(RedisConnection( " redis://... " ))
stack.callback(os.chdir, original_dir)
stack.callback(signal.signal, signal. SIGINT , original_handler)
with open ( " data.txt " ) as f:
# f is closed here — but content is still valid (it's a string)
# However, if you stored the file object:
with open ( " data.txt " ) as f:
f.read() # ValueError: I/O operation on closed file
raise RuntimeError ( " Setup failed " )
return self # Never reached
def __exit__ ( self , * args ):
print ( " Cleanup " ) # NOT called — __enter__ raised
pass # RuntimeError: Setup failed, __exit__ NOT called
If __enter__ raises, __exit__ is never called. Handle this in the caller:
print ( " Setup failed — handle appropriately " )
from contextlib import contextmanager
yield " second " # RuntimeError: generator didn't stop
yield # RuntimeError: generator didn't stop after first yield
The generator must yield exactly once. Multiple yields are a RuntimeError.
def __exit__ ( self , exc_type , exc_val , exc_tb ):
return True # Suppresses ALL exceptions — in most cases wrong
raise ValueError ( " critical error " )
# No exception raised — silently swallowed
Only suppress exceptions intentionally and for specific types. Always log suppressed exceptions:
def __exit__ ( self , exc_type , exc_val , exc_tb ):
if issubclass (exc_type, ( FileNotFoundError , PermissionError )):
logger.warning( f "Suppressed { exc_type. __name__} : { exc_val } " )
# Verbose — manual implementation
def __init__ ( self , path , mode ):
self .file = open ( self .path, self .mode)
def __exit__ ( self , * args ):
# Clean — use @contextmanager
from contextlib import contextmanager
def file_opener ( path , mode ):
# Cleanest — open() is already a context manager
with open (path, mode) as f:
from contextlib import ExitStack
f = stack.enter_context( open ( " data.txt " ))
# If an exception occurs before stack.__enter__(),
with ExitStack() as stack:
f = stack.enter_context( open ( " data.txt " ))
# WRONG — async context manager in sync with
async def get_resource ():
# with get_resource(): # TypeError: async_generator does not support the context manager protocol
async with get_resource():
This topic covers the core concepts of context managers and the with statement, including underlying theory, practical implementation, and key applications.
Key concepts include:
relational databases and SQL normalisation (1NF, 2NF, 3NF) entity-relationship diagrams transaction processing (ACID) NoSQL and distributed databases Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Context managers are like restaurant reservations: you book a table (open a resource), use it for your meal (do your work), and then leave (close the resource) regardless of whether you enjoyed the food or had to rush out because of a fire alarm. The with statement guarantees cleanup happens even when exceptions occur, which is why it replaced the error-prone pattern of manually calling close() in finally blocks. A context manager is a promise that no matter what happens inside the block, the mess will be cleaned up.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
## Cross-References
Advanced Typing : Extends Python’s type system with protocols and generics, which can be used to type context managers more precisely.Data Validation : Shows how to validate data within context managers, ensuring resources receive properly validated input.Protocols and Dunder Methods : Explains the enter and exit dunder methods that define context manager protocol.Async Context Managers : Builds on context managers for asynchronous resource management with async with and async generators.