Learn by reading through in order

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.

Forgetting to close holds the external process's resources too
PythonprocessopenconnectionMySQL / OSholds resourceclose()frees both ✅Python(done running)connectionstill openMySQL / OSstill holdsresourceFD exhausted /connectionlimit hit ❌
Files and DBs hand a resource to the OS or another process outside Python. If Python doesn't call 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.

Execution flow of with X() as y:
with X() as y:enter__enter__calledreturn valuegoes into ybody ofthe block__exit__cleanupreturnexit
On entry, __enter__ runs and its return value goes into the variable named after as. On exit — normal or exceptional — __exit__ is always called for cleanup.

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.

Write the same DatabaseManager from the sample yourself and drive it with with.

① Define class DatabaseManager: and assign self.db_name = db_name and self.connection = None in __init__(self, db_name).

② Define __enter__(self). Print f"Connecting to {self.db_name}", set self.connection = f"connection_to_{self.db_name}", then return self.connection.

③ Define __exit__(self, exc_type, exc_val, traceback). Print f"Disconnecting from {self.db_name}", set self.connection = None, and finally return False.

④ Inside with DatabaseManager("user_data_db") as conn:, print f"active connection: {conn}" and then print("inserting data").

(If your code runs correctly, the explanation will appear.)

Python Editor

Run code to see output

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 exitexc_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.

Concrete values delivered to __exit__'s 3 arguments
what happenedin the withexc_typeexc_valtracebacknormal exit(no exception)NoneNoneNoneraiseValueError("invalid")<class'ValueError'>ValueError('invalid')<tracebackobject>
Normal exit delivers None × 3. Exceptions deliver the "class / instance / traceback" triple. A concrete raise ValueError("invalid") makes the contents tangible.
Normal exit vs exception exit in __exit__
normal exitfrom withexc_type =None, etc.__exit__cleanup onlyexit thewith blockexceptioninside withexc_type / val/ traceback__exit__log + cleanupreturn False-> propagate
When the block raises, the exception's information is packed into __exit__'s three arguments. The return value picks swallow (True) or re-raise (False).

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.

Modify Exercise 1's DatabaseManager to confirm two things: __exit__ runs even on exceptions, and its three arguments receive values.

① Define class DatabaseManager: and assign self.db_name = db_name in __init__(self, db_name).

② In __enter__(self), print f"Enter: {self.db_name}" and return self (so as binds the manager itself).

③ In __exit__(self, exc_type, exc_val, traceback), print all three arguments line by line (print("exc_type:", exc_type) and so on), then print(f"Exit: {self.db_name}"), and finally return False.

④ Inside a try: block, open with DatabaseManager("shop_db"):, print "start", then raise ValueError("bad inventory data").

⑤ Catch with except ValueError as e: and print(f"caught outside: {e}").

Python Editor

Run code to see output

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
with concentrates open/close responsibility in the class
If you go with try / finally
  • Caller — has to write try / finally every time
  • Forgetting — one bad copy-paste and a leak appears
  • Change cost — extra cleanup steps mean editing every call site
with statement + context manager
  • Caller — one line, with X() as y:
  • Forgetting — impossible at the syntax level (__exit__ always runs)
  • Change cost — extra cleanup means editing only __exit__
Separating "resource user" from "open/close owner" is the value 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.

QUIZ

Knowledge Check

Answer each question one by one.

Q1In with X() as y:, the value bound to y is the return value of which method?

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?