Python Context Managers
Context managers aren’t magic; they’re manners. If you borrow a resource, you return it—no excuses. with makes that automatic.
Scope: pure Python examples only (no async, no DB). Goal: master the pattern so later uses (locks, DB sessions, async) feel obvious.
Section 1
1.1 Asking the Right Questions
What is a context manager?
A tiny protocol (__enter__/__exit__) that guarantees setup/cleanup around a block.
Do I need one? Can I survive without it?
You can survive with try/finally; context managers make that safety automatic and readable.
Where do they show up in real life?
Files (open), temp files/dirs, changing dirs, silencing output, timers, locks, mock state.
Should I use them everywhere?
No. Use them when there’s a resource or state that must be restored 100% of the time.
1.2 Context Managers in the Wild (no async, no DB)
Everyday built-ins
- with open("x.txt") as f:— auto close
- with tempfile.TemporaryDirectory() as d:— auto cleanup
- with Path("x").open() as f:— same idea (via- pathlib)
The invisible cleanup they save you from
- File handles closed even on exceptions
- Working directory restored after temporary change
- Stdout/stderr restored after capture
Why try/finally isn’t enough
You’ll forget it at 2 a.m. Context managers don’t forget. (And they’re composable.)
1.3 How to Use Them Safely
Basic with syntax
with open("data.txt") as f:
    data = f.read()
Chaining multiple managers
from pathlib import Path
with Path("in.txt").open() as fin, Path("out.txt").open("w") as fout:
    fout.write(fin.read())
Nesting & readability tips
- Prefer one line per manager when bodies are big.
- Extract big setups into helper functions or your own CM.
Section 2
2.1 Writing Your Own
Class-based approach
import time
class Timer:
    def __enter__(self):
        self.t0 = time.perf_counter()
        return self
    def __exit__(self, exc_type, exc, tb):
        self.elapsed = time.perf_counter() - self.t0
        print(f"[Timer] {self.elapsed:.4f}s")
        return False  # don’t swallow errors
with Timer() as t:
    sum(range(1_000_00))
Decorator approach (@contextmanager)
from contextlib import contextmanager
import os
@contextmanager
def cd(path):
    old = os.getcwd()
    os.makedirs(path, exist_ok=True)
    try:
        os.chdir(path)
        yield
    finally:
        os.chdir(old)
When to choose which
- Class: needs state object, multiple methods, richer API.
- Decorator: quick one-off setup/teardown; very readable.
2.2 Common Mistakes (and fixes)
- Swallowing exceptions in __exit__— ReturningTruehides errors. Default toFalseunless you’re deliberately handling them.
- Failing to restore global state — Always save/restore (cwd,sys.stdout, env vars).
- Over-engineering — If a try/finallyfits in 3 lines and is used once, don’t make a CM “just because.”
Forgetting the decorator
# WRONG: missing @contextmanager
# RIGHT:
@contextmanager
def temp(): ...
2.3 Knowing When (and When Not) to Use Them
Good signals
- You open/lock/redirect/change-dir and must undo it.
- You’ve repeated the same try/finallytwice.
Overkill signs
- No external resource or global state involved.
- One-line operation with no persistent side-effects.
Refactor move
Before:
old = os.getcwd()
try:
    os.chdir("sandbox")
    build()
finally:
    os.chdir(old)
After:
with cd("sandbox"):
    build()
Section 3
3.1 Knowing When (and When Not) — Concrete checklist
- Yes, use CM: files, temp paths, timers, redirected output, cwd changes, lock‑protected counters.
- Probably no: basic math, string munging, pure computations.
- Refactor example: see cdabove.
3.2 Advanced Patterns (kept non‑async for now)
Stacking elegantly
from contextlib import ExitStack
from pathlib import Path
with ExitStack() as stack:
    fin  = stack.enter_context(Path("in.txt").open())
    fout = stack.enter_context(Path("out.txt").open("w"))
    fout.write(fin.read())
Reusable libraries
- Put your CMs in utils/contexts.py(e.g.,Timer,cd,silence_stdout,temp_env).
- Keep them tiny, predictable, and well‑named.
(Deferred) async with — parked until requested.
Common Mistakes with Context Managers
Even experienced devs slip up when first writing their own context managers. Here common pitfalls and how to fix them.
- Forgetting @context manager
from contextlib import contextmanager
def temporary_resource():
  print("Allocating resource")
  try:
    yield "resource"
  finally:
    print("Cleaning up resource")
with temporary_resource() as r:
  print(f"Working with {r}")❌ This fails with:
TypeError: 'generator' object doesn't support the context manager protocol. Why? Because without @ context manager, Python just sees a generator -not a context manager.
✅Fixed version:
from contextlib import contextmanager
@contextmanager
def temporary_resource():
  print("Allocating resource")
  try:
    yield "resource"
  finally:
    print("Cleaning up resource")
with temporary_resource() as r:
  print(f"Working with {r}")- Forgetting to Return a Value in __ enter__
class BrokenManager:
  def __enter__(self):
    print("Entering context")
    #forgot return 
  def __exit__(self, exc_type, exc,tb):
    print("Exiting context")
  with BrokenManager() as r:
    print("Got:", r) # r is None❌Here r becomes None, which might break your code later
✅Fixed version:
class BrokenManager:
  def __enter__(self):
    print("Entering context")
    return "resource"
  def __exit__(self, exc_type, exc,tb):
    print("Exiting context")
  with BrokenManager() as r:
    print("Got:", r) # "resource"Takeaway
. Always use @contextmanager when building function-based managers
. Always return something useful from __ enter __ if you plan to us as r.
- Accidentally Swallowing Exceptions in __ exit __
By default __ exit __ runs cleanup and then re-raises exceptions. But if you return True from __ exit __, Python assumes you "handled it" and suppress the exception.
class SilentManager:
    def __enter__(self): return "resource"
    def __exit__(self, exc_type, exc, tb):
        return True   # ❌ hides ALL exceptions!
With this, even raise ValueError ("Oops!") vanishes silently.✅Fixed version: only suppress when you really mean it:
class CarefulManager:
    def __enter__(self): return "resource"
    def __exit__(self, exc_type, exc, tb):
        if exc_type is ValueError:
            print("Handled ValueError safely")
            return True
        # let others bubble upTakeaway
. Don't return True blindly from __ exit __.
. Suppress only the exceptions you explicitly want to handle.
. Otherwise, bugs vanish silently and debugging becomes a nightmare .
Wrapping Up
Context managers are one of Python’s most elegant and powerful features. They encapsulate resource management, making your code:
Safer: resources are always released.
Cleaner: business logic isn’t cluttered with setup/cleanup details.
More declarative: readers can see at a glance that something special happens in a block.
From built-in managers like open and threading.Lock to advanced patterns involving asynchronous code or dynamic stacks with ExitStack, context managers help you write robust, maintainable code. They are best used for real resource lifecycles—acquiring and releasing something—and not as decorative syntactic sugar.
When you write your own context managers, start simple. Use @contextmanager for concise one-liners. Use classes when you need state or complex logic. Test them thoroughly. Avoid common pitfalls like swallowing exceptions or forgetting to restore state. And perhaps most importantly, know when not to use them. Context managers are a tool for clarity and safety, not a badge of sophistication.
By practicing these patterns and building a library of reusable managers, you’ll write code you can trust tomorrow—code that manages resources safely, handles exceptions gracefully, and stays clear and concise even as your projects grow.
