Python Context Managers: Master the with Statement for Clean Resource Handling

Python Context Managers: Master the with Statement for Clean Resource Handling

Python context managers are the language’s answer to the question “how do I guarantee cleanup even when an exception occurs?” The with statement calls __enter__ on entry and __exit__ on exit — even if an exception is raised. This makes resource management both safe and readable.

TL;DR: with statement calls __enter__ on entry, __exit__ on exit (even on exception). Implement via __enter__/__exit__ methods or @contextmanager decorator. Use contextlib.suppress to ignore specific exceptions. ExitStack for dynamic context managers. Multiple managers in one with statement.

__enter__ and __exit__

class DatabaseConnection:
  def __init__(self, dsn): self.dsn = dsn

  def __enter__(self):
    self.conn = create_connection(self.dsn)
    return self.conn  # Value bound to 'as' variable

  def __exit__(self, exc_type, exc_val, exc_tb):
    if exc_type:  # Exception occurred
      self.conn.rollback()
    else:
      self.conn.commit()
    self.conn.close()
    return False  # False = don't suppress exception
    # Return True to suppress the exception

with DatabaseConnection('postgresql://...') as conn:
  conn.execute('INSERT INTO ...')
  # Auto-commits or rolls back, always closes

@contextmanager — the easy way

from contextlib import contextmanager
import time

@contextmanager
def timer(name=''):
  start = time.perf_counter()
  try:
    yield  # Code inside 'with' block runs here
  finally:     # Runs even on exception
    elapsed = (time.perf_counter()-start)*1000
    print(f'{name}: {elapsed:.1f}ms')

with timer('database query'):
  results = db.execute('SELECT ...')  # Timed!

# Transaction context manager:
@contextmanager
def transaction(db):
  try:
    yield db
    db.commit()
  except:
    db.rollback()
    raise  # Re-raise after rollback

Multiple and nested managers

# Multiple managers in one with:
with open('input.txt') as f_in, open('output.txt','w') as f_out:
  f_out.write(f_in.read())
# Both files closed even if exception!

# contextlib.suppress — ignore specific exceptions:
from contextlib import suppress
with suppress(FileNotFoundError):
  os.remove('/tmp/temp_file')  # OK if file doesn't exist
# Same as: try/except FileNotFoundError: pass (but cleaner)

contextlib.ExitStack — dynamic managers

from contextlib import ExitStack

# Open variable number of files:
file_names = ['a.txt','b.txt','c.txt']
with ExitStack() as stack:
  files = [stack.enter_context(open(f)) for f in file_names]
  # All files open, all will be closed on exit
  data = [f.read() for f in files]

# ExitStack.callback: register any cleanup function
with ExitStack() as stack:
  conn = db.connect()
  stack.callback(conn.close)   # Always closes
  stack.callback(conn.rollback) # Always rolls back (if not committed)
  • ✅ @contextmanager for simple context managers (no class needed)
  • ✅ Multiple managers in one with statement
  • ✅ contextlib.suppress instead of empty except blocks
  • ✅ ExitStack for dynamic or variable numbers of managers
  • ❌ Never return True from __exit__ unless you intentionally suppress exceptions
  • ❌ Never put cleanup in finally when with handles it

External reference: Python contextlib documentation.

Recommended Reading

Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.

The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Join 2,000+ Senior Developers

Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply