Q1In with X() as y:, the value bound to y is the return value of which method?
with Statement and Context Managers — Safe Open/Close with __enter__ / __exit__
Learn Python's with statement and context managers. The __enter__ / __exit__ pair for safe open/close, and how to write your own — hands-on.
Last time we worked on protecting internal class state. This time we step one layer outward — to external resources that live outside the Python process (files, database connections, network sockets, locks) — and look at the with statement and context managers that handle their acquisition and release safely.
Why You Need the with Statement
Operations like opening a file or connecting to a database come with cleanup duty: "once you're done, close it". Forget to close, and you leak file descriptors, hold DB connections forever, and leave the external process clinging to resources too.
close(), the other side keeps waiting for instructions and holding the resource.You can write the same logic with try / finally, but then every author has to remember to call close() inside finally every single time. As the codebase grows or more people touch it, somebody will forget — that's just reality.
The with statement closes acquisition and release into one syntactic unit and automates them. with open("file.txt") as f: is the canonical example: the file is guaranteed to close the moment you leave the with block.
Writing Your Own Context Manager — __enter__ and __exit__
An object that can be used with with is called a context manager. To turn a class into one, just implement two special methods.
- __enter__(self) — runs when you enter the with block. Its return value is bound to the variable named after as.
- __exit__(self, exc_type, exc_val, traceback) — runs when you leave the block. Always called, normal exit or exceptional.
A minimal DB-connection-style example is below (we don't really use a DB library — we mimic it with strings).
class DatabaseManager:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None # not connected yet
def __enter__(self):
print(f"Connecting to {self.db_name}")
self.connection = f"connection_to_{self.db_name}" # real code: actual connection object
return self.connection # value bound to the as variable
def __exit__(self, exc_type, exc_val, traceback):
print(f"Disconnecting from {self.db_name}")
self.connection = None # cleanup
return False # don't swallow exceptions
with DatabaseManager("user_data_db") as conn:
print(f" active connection: {conn}")
print(" inserting data")
# ↑ when this block exits, __exit__ runs
Run it and the output appears in the order "connect → in-block work → disconnect". The disconnect runs without anyone calling it explicitly — that's the whole value of with. The developer is freed from worrying about closing the connection.
The Three Arguments of __exit__ — Catching Exceptions
__exit__ takes three arguments: exc_type, exc_val, traceback. Python uses these to tell __exit__ whether an exception occurred inside the with block.
- Normal exit — all three are None. Just clean up.
- Exception exit — exc_type is the exception class, exc_val is the instance, traceback is the traceback object.
The return value of __exit__ also has meaning. Returning True swallows the exception — it doesn't propagate beyond the block. Returning False / None re-raises after cleanup. The default should be False (or return nothing): log or notify, but always let the exception escape.
None × 3. Exceptions deliver the "class / instance / traceback" triple. A concrete raise ValueError("invalid") makes the contents tangible.Returning True from __exit__ silently kills the exception
If __exit__ returns True, the exception inside with does not propagate. Tempting, but the caller now thinks the operation succeeded — that's a serious side effect. Default to False (or no return): log or notify if you want, but always let the exception bubble up.
Now actually trigger an exception inside with and watch what reaches __exit__'s three arguments.
Compared to try / finally — Why with Wins
A context manager's job can be done with try / finally. The reason to choose with instead is that "the open/close pair lives inside the class". Writing the same task two ways makes the difference in caller code volume and clarity obvious.
# ❌ try / finally — caller hand-writes the cleanup every time
db = DatabaseManager("shop_db")
conn = db.open() # custom connect method
try:
use(conn) # real work
finally:
db.close() # don't forget — copy-pasted everywhere
# ✅ with — open/close lives in the class, caller does only the work
with DatabaseManager("shop_db") as conn:
use(conn) # no finally needed
- Caller — has to write
try/finallyevery time - Forgetting — one bad copy-paste and a leak appears
- Change cost — extra cleanup steps mean editing every call site
- Caller — one line,
with X() as y: - Forgetting — impossible at the syntax level (
__exit__always runs) - Change cost — extra cleanup means editing only
__exit__
with adds. As call sites multiply, cleanup changes still stay inside one class.Use with anywhere acquisition and release pair up
Files, DB connections, locks, network sockets — anywhere you "grab a resource at the start and must give it back at the end" — is a candidate for with. The Python standard library already exposes many of these as context managers: open(), threading.Lock(), sqlite3.connect(), and so on.
Knowledge Check
Answer each question one by one.
Q2When an exception is raised inside a with block, which describes __exit__'s behavior correctly?
Q3If __exit__ returns True, what happens to the exception raised inside the with block?