Python Context Managers

Understand the with statement, write context managers as classes or with @contextmanager, and guarantee clean-up even after errors.

Lesson 42 of 107Advanced12 min read

A context manager is an object that sets something up at the start of a with block and reliably cleans it up at the end, even if the block raises an exception. Files are the best-known example:

Python
with open("report.txt", "w", encoding="utf-8") as file:
    file.write("Clear skies\n")
# the file is closed here, whatever happened inside the block

You can write your own context managers for anything with a "start" and "finish": opening and closing a connection, acquiring and releasing a lock, starting and stopping a timer, or temporarily changing a setting. This lesson explains the protocol behind with, how exceptions reach __exit__, what the as name receives, and the shortcut @contextmanager decorator. The examples follow an observatory opening and closing its dome.

Resource lifetime

Some resources must be released after use: files, network connections, database transactions, locks and temporary directories. Without with, you need try/finally to be sure the release happens:

Python
file = open("log.txt", "a", encoding="utf-8")
try:
    file.write("Session started\n")
finally:
    file.close()

with packages that pattern into one line and makes it impossible to forget:

Python
with open("log.txt", "a", encoding="utf-8") as file:
    file.write("Session started\n")

The clean-up runs when the block finishes normally, when it raises an exception, and when it exits early through return, break or continue. Several managers can share one statement, with open(a) as src, open(b, "w") as dst:, and they are cleaned up in reverse order. From Python 3.10 you can wrap a long list of managers in brackets across several lines.

Protocol methods

Any object with two special methods is a context manager:

  • __enter__(self) runs at the start of the block. Its return value is bound to the name after as.
  • __exit__(self, exc_type, exc_value, traceback) runs at the end. If the block raised an exception, the three arguments describe it; otherwise all three are None.
Python
class ObservingSession:
    def __enter__(self):
        print("Open dome")
        return self
    def __exit__(self, exc_type, exc_value, traceback):
        print("Close dome")
        return False          # don't suppress exceptions

If __exit__ returns a truthy value, Python treats the exception as handled and continues after the with block. Returning False or None lets it propagate as normal. Suppressing exceptions should be deliberate and narrow; a manager that returns True for everything hides bugs. contextlib.suppress(FileNotFoundError) is the standard tool when you really do want to ignore one specific error.

Bound value

The name after as receives whatever __enter__ returns, which is not necessarily the manager itself:

Python
class Telescope:
    def __init__(self, target):
        self.target = target
    def __enter__(self):
        print("Pointing telescope")
        return self.target        # bind the target, not the Telescope
    def __exit__(self, *exc):
        print("Parking telescope")

with Telescope("Jupiter") as target:
    print("Observing", target)

open() returns a file object whose __enter__ returns itself, which is why with open(...) as f gives you the file. A database connection's __enter__, on the other hand, may return a cursor or a transaction. Check the documentation of each manager, and remember that as is optional: with lock: is perfectly valid when you don't need a name.

Generator managers

Writing a class with two methods is verbose for simple cases. contextlib.contextmanager turns a generator function into a context manager: code before yield is the set-up, the yielded value is bound by as, and code after yield is the clean-up.

Python
from contextlib import contextmanager

@contextmanager
def observing_session(target):
    print("Open dome")
    try:
        yield target
    finally:
        print("Close dome")

If the with block raises an exception, it is re-raised at the yield inside your generator. That's why essential clean-up belongs in a finally around the yield: without it, an exception would skip the clean-up line entirely. The generator must yield exactly once; yielding twice, or not at all, raises RuntimeError. To handle the exception rather than just clean up, wrap the yield in try/except.

Independent practice and a worked check

Task: verify that an observing manager closes after successful work and after a raised exception.

Python
from contextlib import contextmanager

log = []

@contextmanager
def observing_session(target):
    log.append(f"open {target}")
    try:
        yield target
    finally:
        log.append(f"close {target}")

with observing_session("Venus"):
    log.append("observe Venus")

try:
    with observing_session("Mars"):
        raise RuntimeError("tracking motor failed")
except RuntimeError as error:
    log.append(f"error: {error}")

print(log)
Text
['open Venus', 'observe Venus', 'close Venus', 'open Mars', 'close Mars', 'error: tracking motor failed']

"close Mars" appears before the error is handled outside, proving the clean-up ran first. For the worked check, essential @contextmanager clean-up goes in finally around yield.

Reference and next practice

The language reference explains the with statement, and contextlib documents contextmanager, suppress and ExitStack. Continue with the file lifecycle and revisit error handling. Practise with the Python exercises and Python quiz.

Context Managers Summary

  • Resource lifetime: with pairs context entry with cleanup when leaving the block.
  • Protocol methods: __enter__ prepares the context and __exit__ handles normal and exceptional exits.
  • Bound value: The result of __enter__ is assigned to the name after as.
  • Generator managers: contextmanager divides setup and cleanup around one yield.

Context Managers Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Context Managers in a small program

Python
from contextlib import contextmanager

@contextmanager
def observing_session():
    print("Open dome")
    try:
        yield "Jupiter"
    finally:
        print("Close dome")

with observing_session() as target:
    print(target)
Output
Open dome
Jupiter
Close dome
Line-by-line explanation
  1. Lines 3 to 9Code before yield is the set-up; the finally clause is the clean-up.
  2. Line 7The yielded value, "Jupiter", is what as target receives.
  3. Lines 11 to 12The block runs between the set-up and the clean-up.
Example 2

Clean-up after an exception

Python
class ObservingSession:
    def __init__(self, target):
        self.target = target

    def __enter__(self):
        print(f"Open dome for {self.target}")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Close dome")
        if exc_type is ValueError:
            print(f"Logged problem: {exc_value}")
            return True              # handled: don't re-raise
        return False

with ObservingSession("Saturn") as session:
    print("Observing", session.target)

with ObservingSession("Mars") as session:
    raise ValueError("cloud cover")
print("Program continues")
Output
Open dome for Saturn
Observing Saturn
Close dome
Open dome for Mars
Close dome
Logged problem: cloud cover
Program continues
Line-by-line explanation
  1. Lines 9 to 14__exit__ always runs. It suppresses only ValueError, and only because it returns True for that type.
  2. Lines 19 to 20The exception interrupts the block, but the dome still closes.
  3. Line 21Because the error was handled, execution continues after the with block.

Common Context Managers Mistakes

Putting clean-up after yield without finally

Don't
@contextmanager
def session():
    print("Open dome")
    yield
    print("Close dome")     # skipped if the block raises
Do
@contextmanager
def session():
    print("Open dome")
    try:
        yield
    finally:
        print("Close dome")

An exception in the block is raised at the yield, so only a finally guarantees the clean-up.

Returning True from __exit__ for every error

Don't
def __exit__(self, exc_type, exc_value, traceback):
    self.close()
    return True             # silently hides every exception, including bugs
Do
def __exit__(self, exc_type, exc_value, traceback):
    self.close()
    return False            # let exceptions propagate

Context Managers in Real Projects

Context managers you will meet

The standard library is full of them: open(), threading.Lock(), tempfile.TemporaryDirectory(), decimal.localcontext(), unittest.mock.patch() and contextlib.redirect_stdout(). Database libraries use with connection: to commit a transaction on success and roll it back on an exception. Testing frameworks use with pytest.raises(ValueError): to assert that a block fails in the expected way.

In your own code, a context manager is worth writing whenever two actions must always come as a pair. contextlib.ExitStack handles a variable number of resources, such as opening every file in a list, and makes sure all of them are closed.

Context Managers FAQ

What is a context manager in Python?

An object that defines __enter__ and __exit__, so it can be used in a with statement. __enter__ runs when the block starts and __exit__ runs when it ends, even if an exception occurred, which makes context managers ideal for releasing resources.

Why use with open() instead of open() and close()?

with closes the file automatically, including when an exception is raised or the function returns early. With manual close(), an error between opening and closing can leave the file open, and buffered data may never be written.

Does __exit__ run if there is an exception?

Yes. __exit__ receives the exception's type, value and traceback. It runs before the exception continues upwards, and if it returns a truthy value the exception is suppressed.

How do I write a context manager with a function?

Decorate a generator function with @contextlib.contextmanager. Put the set-up before a single yield, yield the value for as, and put clean-up in a finally block after it.

Can I use several context managers in one with statement?

Yes: with open("a.txt") as a, open("b.txt", "w") as b:. They are entered left to right and exited in reverse order. From Python 3.10 you can put the managers in brackets and spread them over several lines.

Context Managers Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Where should essential contextmanager cleanup go?

Multiple choicemedium

Which review concern is valid for context managers?

Context Managers Quiz

2 questions to check what you learned.

  1. Where should essential contextmanager cleanup go?

    • Only before yield
    • In finally around yield
    • Python ignores the distinction
  2. Which statement correctly describes bound value?

    • The result of __enter__ is assigned to the name after as.
    • Only the example output matters, not the rule
    • Every operation has the same behavior