The with statement

Opening a file, acquiring a lock, starting a database transaction — every "set up, use, then always tear down" pattern. Java solved it with try-with-resources; Python's version is the with block, powered by two dunder methods: __enter__ and __exit__. The promise: cleanup runs even if the body blows up.

with__enter__ / __exit__ file I/Oguaranteed cleanup RAII

The problem: forgetting to clean up

If you open() a file and just read from it, the file stays open until the garbage collector happens to get to it — and if an exception fires mid-read, you might leak the handle entirely. The careful way is a try/finally, but writing that around every resource is noisy and easy to forget. The with statement bottles that pattern up:

with open("data.csv") as f:  # __enter__ → gives you f
    rows = f.read()         # the body, using the resource
# ← f is closed automatically here, even if read() raised

How it works: __enter__ then __exit__, always

A context manager is any object with two methods. On entering the block, Python calls __enter__() and binds its return value to the as name. On leaving the block — by finishing, by return, or by an exception — Python guarantees a call to __exit__(). That guarantee is the whole point. Toggle whether the body raises and watch __exit__ fire either way:

Comparing the two: with vs hand-written try/finally

They do the same thing — with is just the distilled, can't-forget-it version:

verbose & forgettable:

f = open("data.csv")
try:
    rows = f.read()
finally:
    f.close()

idiomatic:

with open("data.csv") as f:
    rows = f.read()

# close() is automatic

You can write your own with the @contextmanager decorator (a generator that yields once — everything before the yield is "enter," everything after is "exit"), but for now the win is simple: wrap every file, lock, or connection in a with and you'll never leak one.

What __exit__ decides, and the shortcuts

__exit__(exc_type, exc_value, tb) receives the exception that's in flight (all three are None on a clean exit) and its return value is a decision: return anything truthy and Python treats the exception as handled — it stops right there, as if you'd written a matching except; return False (or nothing — a bare function returns None, which is falsy) and the exception propagates, exactly as if __exit__ weren't there. That single boolean is the whole reason with can replace both "close no matter what" and "suppress this specific error" — it's not two features, it's one return value. Spelled out, the statement desugars to roughly this (the real version is in PEP 343):

mgr = EXPR
f = mgr.__enter__()
clean = True
try:
    try:
        BODY
    except BaseException:
        clean = False
        if not mgr.__exit__(*sys.exc_info()): raise
finally:
    if clean: mgr.__exit__(None, None, None)  # also on return / break

The @contextmanager shortcut from above is that whole class generated from one yield — but the mechanism is worth seeing once, because it's not "the function resumes": if the body raises, Python throws that exception into the generator at the yield line, so a try/finally wrapped around the yield is what actually guarantees cleanup, exactly as a real __exit__ would.

Two idioms fall straight out of this contract. ExitStack lets you enter a variable number of context managers and closes them in reverse order — same guarantee as nesting with blocks by hand, without knowing the count up front. And atomic write — write to a temp file, then os.replace() it over the real path only once the write finished clean — turns "guaranteed cleanup" into "guaranteed no half-written file," because an exception before the rename never touches the original at all.

Takeaways: with mgr as x: calls mgr.__enter__() (its result → x), runs the body, then always calls mgr.__exit__() — on success or exception. It replaces error-prone try/finally cleanup. Use it for files, locks, connections, transactions. Builds on dunder methods and exceptions.