Lesson 18 • Advanced
Context Managers & the with Statement In Depth
Context managers are one of Python's most elegant and powerful tools — used everywhere from file handling, database transactions, network requests, locks, concurrency, and resource safety.
What You'll Learn in This Lesson
- • What a context manager is and how
withworks under the hood - • Creating class-based context managers with
__enter__and__exit__ - • Using
@contextmanagerfromcontextlibfor simpler patterns - • Handling errors inside context managers gracefully
- • Real-world uses: database sessions, file I/O, timers, locks
What You'll Learn
If you've ever written with open("file.txt") as f: …you've already used a context manager.
This lesson teaches you:
✔ How context managers work
✔ How to create your own using classes
✔ How to create them using generator functions (contextlib.contextmanager)
✔ How real systems use them for safe resource handling
✔ How to build production-grade context managers
✔ How to combine context managers with decorators & advanced design patterns
🔥 1. What Is a Context Manager?
with block), they give you a room key (the resource). When you leave (exit the block), they automatically handle checkout — cleaning the room, billing, etc. Even if you leave early due to an emergency, checkout still happens properly!A context manager controls a setup phase and a cleanup phase.
| Phase | What Happens | Hotel Analogy |
|---|---|---|
| __enter__ | Resource is acquired/prepared | Check in, get room key |
| with block | Your code runs using the resource | Enjoy your stay |
| __exit__ | Resource is cleaned up/released | Checkout, room cleaned |
The common structure:
Context Manager Structure
Basic with statement structure
with something as value:
# do workThe moment execution enters the with block, the context manager prepares a resource. When the block exits — even if an error occurs — the resource is cleaned up.
Typical use cases:
- Files
- Network connections
- Database sessions
- Locks (threading, multiprocessing)
- Temporary directories
- Timers
- Log writers
- Mocking in unit tests
⚙️ 2. How Python Processes the with Statement
Under the hood:
With Statement
Simple with statement usage
with manager as x:
work()is equivalent to:
With Statement Equivalent
What happens under the hood
manager_obj = manager.__enter__()
try:
x = manager_obj
work()
finally:
manager.__exit__(*sys.exc_info())So a context manager must define:
✔ __enter__(self)
- Runs when entering the block
- Returns an optional value
✔ __exit__(self, exc_type, exc, tb)
- Runs when leaving
- Cleans up resources
- Can suppress errors by returning True
🧠 3. Creating Your Own Context Manager (Class-Based)
Let's build a simple context manager that logs entering/exiting:
Class-Based Context Manager
Creating a logger context manager
class Logger:
def __enter__(self):
print("Entering block...")
return "Ready!"
def __exit__(self, exc_type, exc, tb):
print("Exiting block...")
if exc:
print("Error occurred:", exc)
return False # do not suppress errorsUsage:
Using Logger Context Manager
Using the custom logger
with Logger() as msg:
print(msg)Output:
Entering block... Ready! Exiting block...
🧩 4. A Real Project Example — Timing Block Execution
Timer Context Manager
Timing code execution
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, exc_type, exc, tb):
self.end = time.time()
print(f"⏱ Elapsed: {self.end - self.start:.4f}s")Usage:
Using Timer
Timing a computation
with Timer():
sum([x for x in range(5000000)])🧨 5. A More Advanced Example — Database Connections
This pattern exists in ORMs like SQLAlchemy & Django:
Transaction Context Manager
Database transaction pattern
class Transaction:
def __enter__(self):
print("Start transaction")
return self
def __exit__(self, exc_type, exc, tb):
if exc_type:
print("Rollback")
else:
print("Commit")Usage:
Using Transaction
Using the transaction manager
with Transaction():
print("Updating user...")🧊 6. Using contextlib.contextmanager (Generator Context Managers)
For simpler cases, Python provides a shortcut.
Generator Context Manager
Using contextlib.contextmanager
from contextlib import contextmanager
@contextmanager
def open_file(path):
f = open(path)
try:
yield f # __enter__()
finally:
f.close() # __exit__()Usage:
Using Generator Context Manager
Using the file opener
with open_file("data.txt") as file:
print(file.read())This is extremely common in:
- testing tools
- temporary environment changes
- IO helpers
- locking functions
- resource wrappers
🔧 7. Building a Temporary Directory Context Manager
Temporary Directory Manager
Auto-cleanup temp directory
import tempfile
import shutil
from contextlib import contextmanager
@contextmanager
def tempdir():
path = tempfile.mkdtemp()
try:
yield path
finally:
shutil.rmtree(path)Usage:
Using Temp Directory
Using the temp directory manager
with tempdir() as path:
print("Working in", path)🔐 8. Context Managers for Locking & Thread Safety
Example using threading lock:
Thread Lock Context Manager
Thread-safe locking
from threading import Lock
lock = Lock()
with lock:
print("Critical section")Locks are native context managers. This pattern protects shared memory in concurrent programs.
🧬 9. Context Managers Used With Decorators (Advanced Pattern)
You can combine context managers with decorators to automatically wrap function execution:
Decorator + Context Manager
Combining patterns
from contextlib import contextmanager
from functools import wraps
import time
@contextmanager
def timer_block():
start = time.time()
yield
print(f"Elapsed: {time.time() - start:.4f}s")
def timed(fn):
@wraps(fn)
def wrapper(*a, **k):
with timer_block():
return fn(*a, **k)
return wrapper
@timed
def process():
time.sleep(0.2)Usage:
Using Timed Decorator
Calling the timed function
process()🧪 10. Suppressing Errors (But Carefully!)
If __exit__ returns True, exceptions are swallowed.
Example:
Error Suppressing Manager
Suppressing exceptions
class IgnoreErrors:
def __exit__(self, exc_type, exc, tb):
return TrueUsage:
Suppressing Division Error
Error is silently handled
with IgnoreErrors():
1 / 0 # error suppressedBe careful — this is useful ONLY for:
- logging
- cleanup tasks
- retry logic
- safe shutdown
Never suppress runtime errors silently in real systems.
📚 11. Nested Context Managers
Old way:
Nested Context Managers (Old)
Traditional nested approach
with A() as a:
with B() as b:
...Modern way:
Nested Context Managers (Modern)
Clean single-line approach
with A() as a, B() as b:
...This is cleaner and avoids deep nesting.
⚡ 12. Combining Multiple Context Managers Dynamically
Useful for frameworks or plugin systems:
ExitStack for Dynamic Managers
Managing multiple resources dynamically
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in file_list]
# all files now managed safelyThis is extremely powerful for:
- ML pipelines
- batch processing
- plugin environments
🏗 13. Context Managers in Real Frameworks
✔ FastAPI dependency injection
FastAPI DB Dependency
Async database context
async def db():
async with engine.begin() as conn:
yield conn✔ PyTorch CUDA device switching
PyTorch No Grad
Disabling gradient computation
with torch.no_grad():
...✔ Django transaction atomicity
Django Atomic Transaction
Database transaction safety
with transaction.atomic():
...✔ NumPy print options
NumPy Print Options
Temporary print formatting
with np.printoptions(suppress=True):
...Context managers are everywhere.
🧱 14. Build a Production-Ready Context Manager
Let's create a file-write-safe manager:
Safe File Writer
Atomic file writing pattern
class SafeWriter:
def __init__(self, path):
self.path = path
def __enter__(self):
self.temp = open(self.path + ".tmp", "w")
return self.temp
def __exit__(self, exc_type, exc, tb):
self.temp.close()
if exc_type:
# remove corrupted temp file
import os
os.remove(self.path + ".tmp")
return False
import os
os.replace(self.path + ".tmp", self.path)This guarantees:
- No half-written files
- Atomic writes
- OS-level safety
This is similar to how:
- VS Code
- Config managers
- Database migrations
work internally.
🎓 15. Mini Project — Build Your Own Resource Manager
Create a context manager that:
- ✔ tracks memory before/after
- ✔ logs execution time
- ✔ logs exceptions
- ✔ sends results to a log file
- ✔ optionally suppresses errors
Usage:
Monitor Context Manager
Build your own resource monitor
with Monitor("log.txt", suppress=False):
run_expensive_task()This gives you portfolio-ready experience.
🎉 Conclusion
You now understand context managers:
✔ How the with statement works internally
✔ How to build class-based managers
✔ How to build generator-based managers
✔ How to combine decorators and context managers
✔ How to suppress or inspect exceptions
✔ How they power major frameworks
✔ How to create production-grade resource managers
📋 Quick Reference — Context Managers
| Syntax | What it does |
|---|---|
| with open(f) as fh: | Open file safely, auto-closes on exit |
| __enter__(self) | Called when entering the with block |
| __exit__(self, *args) | Called on exit, even if an error occurs |
| @contextmanager | Build a manager from a generator function |
| contextlib.suppress(E) | Ignore a specific exception type |
🏆 Lesson Complete!
You can now build and use context managers for any resource lifecycle — files, databases, locks, timers, and more.
Up next: Generators — produce values lazily for memory-efficient data pipelines.
Sign up for free to track which lessons you've completed and get learning reminders.