A context manager is just an object that knows how to set something up and how to clean it up afterwards — no matter what happens in between . The with statement is Python's promise: "I'll always run your cleanup, even if the code inside blows up." That's the whole idea. Files, locks, DB connections, timers — all the same pattern.
with open ( "data.txt" ) as f: # f = open(...).__enter__()
data = f.read()
# f.__exit__(...) runs here, closing the file — even if read() raised
Without with, you must remember to clean up and wrap everything in try/finally. People forget. Forgetting to close a file leaks a handle; forgetting to release a lock deadlocks your program. Context managers move the cleanup next to the setup, so you can't forget — the language guarantees it.
The statement
with EXPR as VAR :
BODY
is exactly equivalent to this de-sugared form:
mgr = EXPR
VAR = mgr. __enter__ () # ← return value goes to `as VAR`
try :
BODY
except BaseException :
# __exit__ gets the live exception info
if not mgr. __exit__ ( * sys.exc_info()):
raise # re-raise unless __exit__ returned True
else :
mgr. __exit__ ( None , None , None ) # no exception → all three args are None
Intuition Why this exact shape?
__enter__ runs once , before the body — that's "setup".
The try guarantees __exit__ runs whatever happens — that's "guaranteed cleanup".
__exit__ is told whether an exception occurred (via its 3 args), so it can decide: log it? swallow it? clean up differently?
The return value of __exit__ is a "should I swallow the exception?" flag. Return True → exception suppressed. Return False/None → exception propagates.
Worked example A timer context manager (class form)
import time
class Timer :
def __enter__ (self):
self .start = time.perf_counter()
return self # so `as t` gives us the Timer
def __exit__ (self, exc_type, exc_value, tb):
self .elapsed = time.perf_counter() - self .start
print ( f "Took {self .elapsed :.4f } s" )
return False # don't swallow exceptions
with Timer() as t:
sum ( range ( 10_000_000 ))
# prints: Took 0.18s
Why return self? Because we want t to be the Timer object so we can read t.elapsed later. If we returned nothing, t would be None.
Why return False? We want errors inside the block to still propagate — a timer shouldn't hide bugs.
Worked example Suppressing a specific error
class ignore :
def __init__ (self, * exc): self .exc = exc
def __enter__ (self): return self
def __exit__ (self, et, ev, tb):
# Why check et is not None? An exception only occurred if et is set.
return et is not None and issubclass (et, self .exc)
with ignore( FileNotFoundError ):
open ( "nope.txt" ) # error raised...
print ( "still here" ) # ...but swallowed → this prints
Why this step? issubclass(et, self.exc) returns True only for the errors we asked to ignore, so __exit__ returns True and the with swallows them. Other errors return False and propagate. (This is essentially contextlib.suppress.)
Worked example The easy way:
@contextmanager
from contextlib import contextmanager
@contextmanager
def open_upper (path):
f = open (path)
try :
yield f.read().upper() # everything before yield = __enter__
finally :
f.close() # everything after = __exit__
with open_upper( "data.txt" ) as text:
print (text)
Why yield? The single yield splits the function into "setup" (before) and "cleanup" (after). The yielded value becomes the as variable. Why finally? So the file closes even if the body raises.
with returns the file object, so f is the manager."
Why it feels right: with open(...) as f and f is the file, and the file is a context manager — looks like the same thing.
The fix: f is whatever __enter__ returns , not the manager itself. For files they happen to be the same object, but for open_upper above the manager is a generator and text is a string . Always ask: "what does __enter__ return?"
Common mistake "Returning nothing from
__exit__ swallows the error."
Why it feels right: You did your cleanup, so surely the error is handled?
The fix: __exit__ returning None (the default) is falsy → exception propagates . You must explicitly return True to suppress. This is good: silent error-swallowing is a top source of bugs.
__exit__ won't run if the block raises."
Why it feels right: Errors usually skip remaining code.
The fix: That's the entire point of with — __exit__ runs via a try/finally, so it always runs. Setup happened, so cleanup is guaranteed.
return between __enter__'s two halves in a @contextmanager."
Why it feels right: It's a normal function.
The fix: A @contextmanager generator must yield exactly once . Zero yields → RuntimeError: didn't yield; two yields → RuntimeError: generator didn't stop.
Recall Feynman: explain to a 12-year-old
Imagine borrowing a library book. When you walk in (__enter__) you check the book out. When you leave (__exit__) you must return it — even if you trip and fall on the way out. The with block is the librarian standing at the door making sure you never leave with the book. You don't have to remember to return it; the rule does it for you.
Mnemonic Remember the pair
"ENTER sets, EXIT resets." And the 3 EXIT arguments spell the crash report: T ype, V alue, T raceback (TVT). Return T rue to T oss the error away.
What two methods make an object a context manager?
Where does the as variable's value come from?
What does returning True from __exit__ do?
Does __exit__ run if the body raises?
What two dunder methods define a context manager? __enter__ and __exit__
What is bound to the as variable in a with statement? The return value of __enter__
What are the three arguments passed to __exit__? exc_type, exc_value, traceback (None×3 if no exception)
What does __exit__ returning a truthy value do? Suppresses (swallows) the exception that occurred in the block
Does __exit__ run when the with body raises an exception? Yes — it always runs, like a finally block
In @contextmanager, what splits setup from cleanup? A single yield; code before is __enter__, code after (in finally) is __exit__
Why put cleanup in a finally inside a @contextmanager? So cleanup runs even if the with-block body raises an exception
What is with EXPR as VAR: de-sugared to? mgr=EXPR; VAR=mgr.enter (); try BODY; exit runs in try/finally with exc info
contextlib helper that ignores given exception types? contextlib.suppress(*exceptions)
Exception handling — try-except-finally — with is sugar over try/finally.
Decorators — @contextmanager is a decorator that converts a generator into a manager.
Generators and yield — the single-yield trick behind @contextmanager.
File I/O — the canonical with open(...) use case.
Threading and locks — with lock: guarantees release.
Dunder methods — __enter__/__exit__ are part of the protocol family.
Files, locks, timers, DB conns
Intuition Hinglish mein samjho
Dekho, context manager ka idea bahut simple hai: koi bhi cheez jo automatically "setup" aur "cleanup" handle kare. Jaise file kholi toh band bhi karni padti hai, lock liya toh release bhi karna padta hai. Agar tum manually karoge toh kabhi na kabhi bhool jaoge — aur file handle leak ya deadlock ho jayega. with statement Python ka promise hai ki cleanup hamesha chalega, chahe beech mein error aa jaye.
Andar do dunder methods kaam karte hain. __enter__ block start hone par chalta hai, aur jo woh return karta hai wahi as ke baad wale variable mein aata hai. __exit__ block khatam hone par chalta hai — chahe normally ya exception se. Sabse important baat: __exit__ ko teen cheezein milti hain (exc_type, exc_value, traceback). Agar koi error nahi hua toh teeno None hote hain. Aur agar __exit__ se True return karoge toh error "nigl" liya jaata hai (suppress), warna error aage propagate ho jaata hai.
Internally with bas try/finally ka sugar hai — yahi reason hai ki cleanup guaranteed chalti hai. Class wala tareeka thoda lamba hai, isliye @contextmanager decorator use karte hain: ek single yield likho — yield se pehle ka code __enter__ hai, baad ka (finally mein) __exit__. Exam aur real code dono mein yeh pattern bahut aata hai, toh "ENTER sets, EXIT resets" yaad rakhna.