Quick Answer

with calls __enter__ before the block and __exit__ after it, guaranteed — including when an exception is raised. That guarantee is why it is the correct way to handle files, locks and connections.

The problem it solves

Opening a file means you must close it. The naive version does not:

f = open("data.txt")
data = f.read()      # if this raises, close() never runs
f.close()

Any exception between the two lines leaks the file handle. The correct manual version needs try/finally:

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

That is verbose enough that people skip it, which is precisely why with exists:

with open("data.txt") as f:
    data = f.read()
# closed here, whatever happened

Leaked handles are not theoretical. A long-running process that leaks database connections exhausts the pool and stops serving requests — a real and frequently-seen outage.

The protocol is two methods

class Timer:
    def __enter__(self):
        print("enter")
        return self                      # bound to the 'as' name

    def __exit__(self, exc_type, exc, tb):
        print("exit runs even on error:",
              exc_type.__name__ if exc_type else "no error")
        return False                     # do not suppress exceptions
with Timer():
    print("inside")
# enter / inside / exit runs even on error: no error

Now with an exception:

try:
    with Timer():
        raise ValueError("boom")
except ValueError:
    print("exception propagated out")
# enter
# exit runs even on error: ValueError
# exception propagated out

__exit__ ran, received the exception details, and the exception still propagated. That is the guarantee — cleanup happens, and the error is not hidden.

The return value of __exit__ matters

Returning False (or None, which is the default) lets the exception propagate. Returning True swallows it, and the code after the with block continues as if nothing went wrong.

That is occasionally what you want — contextlib.suppress(FileNotFoundError) is built on exactly this. But accidentally returning a truthy value from __exit__ creates a context manager that silently hides every error inside it, which is an unpleasant bug to track down.

Be explicit and return False unless you genuinely intend suppression.

The easier way: @contextmanager

Writing a class for something simple is heavy. contextlib lets a generator do it:

from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")
    yield
    print(f"</{name}>")

with tag("b"):
    print("bold")
# <b> / bold / </b>

Everything before yield is the setup, everything after is the cleanup, and whatever you yield is bound by as.

One caveat: if the block raises, the exception is thrown at the yield, so code after it does not run unless you wrap it:

@contextmanager
def safe_tag(name):
    print(f"<{name}>")
    try:
        yield
    finally:
        print(f"</{name}>")

Use try/finally around the yield whenever the cleanup genuinely must happen — which is usually the entire point.

Where to use your own

Anything with a matched pair of setup and teardown:

  • Timing a block — record the start in __enter__, print the elapsed time in __exit__.
  • Temporarily changing state — switch a setting, restore it afterwards even on failure.
  • Database transactions — commit on success, roll back on exception, using the exc_type argument to decide.
  • Acquiring and releasing a lock — Python's threading.Lock is already a context manager for this reason.
  • Temporary directoriestempfile.TemporaryDirectory() creates and removes one.

Several can be combined in one statement, and they nest in order:

with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())

The signal that you want one: whenever you write try/finally and the same pairing appears more than once, that is a context manager asking to exist. See Python exception handling for the surrounding machinery.

Frequently Asked Questions

What does the with statement actually do? It calls __enter__ before the block and __exit__ after it, guaranteed, including when the block raises. That guarantee is what makes it correct for files, locks and connections.
Does __exit__ run if an exception is raised? Yes, always. It receives the exception type, value and traceback, so it can decide whether to clean up differently — committing or rolling back a transaction, for example.
How do I suppress an exception in a context manager? Return True from __exit__. Be cautious, since an accidental truthy return hides every error inside the block. contextlib.suppress does this deliberately for named exception types.
Class or @contextmanager? @contextmanager is shorter and fine for simple setup and teardown. A class is better when you need state, several methods, or reuse across many call sites.
Can I use multiple context managers in one with statement? Yes, separated by commas. They enter left to right and exit in reverse order, which is equivalent to nesting them.