By the end of this chapter, you should understand:
- What a context manager is.
- Why the
withstatement exists. - How
withrelates totryandfinally. - What
__enter__does. - What
__exit__does. - What gets assigned after
as. - How context managers handle exceptions.
- Why returning
Truefrom__exit__suppresses exceptions. - Why most
__exit__methods should returnFalseorNone. - How files use context management.
- How locks, transactions, timers, and temporary state use context management.
- How to write a class-based context manager.
- How to write a generator-based context manager with
contextlib.contextmanager. - How
contextlib.closing,suppress,nullcontext, andExitStackfit in. - How multiple context managers work in one
withstatement. - How context managers improve resource lifetime design.
- Which context manager mistakes are common.
Chapter 59 studied generators.
Generators produce values over time.
They can also need cleanup.
Context managers solve a related but different problem:
run setup before a block
run cleanup after the block
The most familiar example is files:
with open("data.txt") as file:
text = file.read()This opens the file, gives you the file object, and closes the file when the block ends.
The block may end normally.
The block may end because of an exception.
Either way, the context manager gets a chance to clean up.
That is the central promise of context managers.
Many programs need this pattern:
resource = acquire_resource()
try:
use(resource)
finally:
release_resource(resource)The finally block is important.
It runs even if an error happens.
Example:
file = open("data.txt")
try:
text = file.read()
finally:
file.close()This is correct.
But it is repetitive.
It also separates the resource-management idea across several lines.
The with statement expresses the pattern directly:
with open("data.txt") as file:
text = file.read()This is shorter.
More importantly, it is clearer:
use this resource for this block
clean it up when the block ends
Context managers package setup and cleanup into reusable objects.
An object is a context manager if it implements:
__enter__
__exit__Basic shape:
class Manager:
def __enter__(self):
...
return value
def __exit__(self, exc_type, exc, traceback):
...
return False__enter__ runs before the block.
__exit__ runs after the block.
If the block exits because of an exception, __exit__ receives exception information.
If the block exits normally, __exit__ receives:
None, None, NoneThe return value of __exit__ controls exception suppression.
If __exit__ returns a true value, the exception is suppressed.
If it returns a false value, the exception propagates.
Most context managers should not suppress exceptions.
So most __exit__ methods return:
Falseor simply return nothing, which means None, a false value.
Example:
class Announce:
def __enter__(self):
print("entering")
return self
def __exit__(self, exc_type, exc, traceback):
print("leaving")Use:
with Announce() as manager:
print("inside")Output:
entering
inside
leaving
The sequence is:
create manager
call __enter__
run block
call __exit__
The object returned by __enter__ is assigned to the name after as.
In this example:
managerreceives:
selfbecause __enter__ returned self.
This is subtle.
In:
with expression as target:
...target receives the return value of __enter__, not necessarily the context manager object itself.
Example:
class GivesList:
def __enter__(self):
return []
def __exit__(self, exc_type, exc, traceback):
print("done")Use:
with GivesList() as items:
items.append("a")
items.append("b")
print(items)Output:
['a', 'b']
done
The manager is a GivesList object.
The as target is a list.
This is how open() works too.
The context expression creates a file context manager.
The value after as is the file object used inside the block.
Often they are the same object.
But they do not have to be.
This:
with manager as value:
body(value)roughly means:
enter = manager.__enter__
exit = manager.__exit__
value = enter()
try:
body(value)
except:
if not exit(*exception_info):
raise
else:
exit(None, None, None)The real semantics are precise and use special method lookup, but this model is useful.
Important guarantees:
- the context expression is evaluated
__enter__is called- if
__enter__succeeds,__exit__will be called - if the block raises, exception details are passed to
__exit__ - if
__exit__returns true, the exception is suppressed - otherwise, the exception continues
The most important practical rule:
if __enter__ finishes successfully, __exit__ gets a chance to run
If __enter__ raises an exception, the block does not run.
Also, that context manager's __exit__ is not called because entering did not complete successfully.
Example:
class BrokenEnter:
def __enter__(self):
print("entering")
raise RuntimeError("cannot enter")
def __exit__(self, exc_type, exc, traceback):
print("leaving")Use:
with BrokenEnter():
print("inside")Output:
entering
Then RuntimeError propagates.
inside does not print.
leaving does not print.
If acquisition is partially completed inside __enter__, the __enter__ method itself must clean up before raising.
Context managers must be careful during setup.
__exit__ receives three arguments:
exc_type
exc
tracebackIf no exception occurred:
exc_type is None
exc is None
traceback is NoneIf an exception occurred, they describe it.
Example:
class ShowException:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
print(f"exc_type = {exc_type}")
print(f"exc = {exc}")
return FalseUse:
with ShowException():
raise ValueError("bad")Output includes:
exc_type = <class 'ValueError'>
exc = bad
Then the ValueError continues because __exit__ returned false.
This lets a context manager log, transform, clean up after, or suppress exceptions.
Suppressing should be rare and deliberate.
If __exit__ returns a true value, Python suppresses the exception.
Example:
class SuppressValueError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
return exc_type is ValueErrorUse:
with SuppressValueError():
raise ValueError("ignored")
print("continued")Output:
continued
The ValueError was suppressed.
But:
with SuppressValueError():
raise TypeError("not ignored")propagates because __exit__ returns false.
Suppression can be useful.
But silent exception suppression can hide bugs.
If a context manager suppresses exceptions, its name should make that obvious.
This is the normal pattern:
def __exit__(self, exc_type, exc, traceback):
cleanup()
return Falseor:
def __exit__(self, exc_type, exc, traceback):
cleanup()Because returning None is false.
Files do not suppress exceptions:
with open("data.txt") as file:
raise ValueError("bad")The file is closed.
The exception still propagates.
That is usually what you want:
cleanup should happen
errors should remain visible
Suppress only when the context manager's purpose is suppression or recovery.
The classic context manager is a file:
with open("data.txt") as file:
data = file.read()This ensures the file is closed when the block ends.
Equivalent idea:
file = open("data.txt")
try:
data = file.read()
finally:
file.close()The with version is cleaner.
It also makes the lifetime visible:
the file is meant to be used only inside this block
After the block:
file.closedis:
TrueDo not rely on garbage collection to close files.
Use context managers for deterministic cleanup.
Locks are another common example.
Without with:
lock.acquire()
try:
update_shared_state()
finally:
lock.release()With with:
with lock:
update_shared_state()The lock is acquired before the block and released after the block.
This matters because exceptions should not leave locks held.
Context managers are excellent for paired operations:
acquire / release
open / close
begin / commit-or-rollback
set / restore
enter / leave
When you see paired lifecycle operations, consider a context manager.
Database transactions often fit context managers.
Conceptual example:
with database.transaction() as transaction:
transaction.insert(user)
transaction.insert(profile)Possible behavior:
- begin transaction in
__enter__ - commit if block succeeds
- roll back if block raises
- close transaction resources in
__exit__
Simplified class:
class Transaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin()
return self
def __exit__(self, exc_type, exc, traceback):
if exc_type is None:
self.connection.commit()
else:
self.connection.rollback()
return FalseThe exception is not suppressed.
Rollback happens.
Then the error continues.
This is usually correct.
The context manager protects consistency without hiding failure.
Context managers are useful for temporary changes.
Example:
class TemporaryValue:
def __init__(self, obj, name, value):
self.obj = obj
self.name = name
self.value = value
def __enter__(self):
self.old_value = getattr(self.obj, self.name)
setattr(self.obj, self.name, self.value)
return self
def __exit__(self, exc_type, exc, traceback):
setattr(self.obj, self.name, self.old_value)Use:
settings.debug = False
with TemporaryValue(settings, "debug", True):
run_debug_code()
print(settings.debug)The old value is restored even if run_debug_code() raises.
This pattern appears in:
- tests
- configuration overrides
- environment changes
- warning filters
- decimal precision
- temporary working directories
Temporary changes should be restored reliably.
Context managers make the lifetime explicit.
Timing a block is a good context manager example.
from time import perf_counter
class Timer:
def __enter__(self):
self.start = perf_counter()
return self
def __exit__(self, exc_type, exc, traceback):
self.end = perf_counter()
self.elapsed = self.end - self.startUse:
with Timer() as timer:
total = sum(range(1_000_000))
print(timer.elapsed)__enter__ records the start.
__exit__ records the end.
The timer object remains available after the block.
This is a useful pattern:
return self from __enter__ when the caller should inspect manager state
Sometimes the manager should return something else.
Example:
class ManagedList:
def __init__(self):
self.items = []
def __enter__(self):
return self.items
def __exit__(self, exc_type, exc, traceback):
print(f"collected {len(self.items)} items")Use:
with ManagedList() as items:
items.append("a")
items.append("b")The as variable is the list.
The manager still exists behind the scenes.
This design is good when the block should work with a resource rather than the manager wrapper.
Files work this way conceptually.
The block wants the file object.
The context manager handles lifecycle.
You can use multiple context managers in one with statement:
with open("input.txt") as source, open("output.txt", "w") as target:
target.write(source.read())This is equivalent to nesting:
with open("input.txt") as source:
with open("output.txt", "w") as target:
target.write(source.read())Entering happens left to right.
Exiting happens right to left.
That mirrors nested blocks.
If the second context manager fails to enter, the first one is exited.
This is one reason multiple context managers in one line are safe for common paired resources.
Use parentheses for readability when the line is long:
with (
open("input.txt") as source,
open("output.txt", "w") as target,
):
target.write(source.read())__exit__ runs when leaving the block, even if the block uses return.
Example:
def read_first_line(path):
with open(path) as file:
return file.readline()The file is still closed.
This matters because block exit can happen through:
- normal completion
- exception
returnbreakcontinue
The context manager gets its exit call.
This is the same spirit as finally.
The block's control flow does not skip cleanup.
The with statement uses special method lookup for __enter__ and __exit__.
This is similar to other dunder protocols.
That means defining __enter__ on an individual instance in a casual way is not the normal route.
Define context manager methods on the class:
class Manager:
def __enter__(self):
...
def __exit__(self, exc_type, exc, traceback):
...This follows the data model pattern from Chapter 52.
Protocol methods belong on the type.
Objects then participate in language syntax.
Class-based context managers are explicit.
But simple context managers can be written with a generator and contextlib.contextmanager.
Example:
from contextlib import contextmanager
@contextmanager
def announce():
print("entering")
try:
yield
finally:
print("leaving")Use:
with announce():
print("inside")Output:
entering
inside
leaving
The code before yield is setup.
The yielded value is assigned after as, if any.
The code after yield is cleanup.
The try/finally ensures cleanup runs when exceptions occur.
This connects directly to Chapter 59.
Example:
from contextlib import contextmanager
@contextmanager
def managed_list():
items = []
try:
yield items
finally:
print(f"collected {len(items)} items")Use:
with managed_list() as items:
items.append("a")
items.append("b")The yielded items list is assigned to the as target.
This:
yield itemsplays the role of __enter__ returning a value.
The code after yield plays the role of __exit__.
Generator-based context managers are concise for simple setup/cleanup.
Class-based context managers are better when behavior needs more structure or multiple methods.
A function decorated with @contextmanager must yield exactly once.
This is correct:
@contextmanager
def manager():
setup()
try:
yield resource
finally:
cleanup()This is wrong:
@contextmanager
def manager():
setup()
cleanup()It never yields.
This is also wrong:
@contextmanager
def manager():
yield "first"
yield "second"A context manager manages one block.
It enters once and exits once.
The generator should yield once at the boundary between setup and cleanup.
If the with block raises, the exception is thrown into the generator at the yield point.
Example:
@contextmanager
def show_error():
try:
yield
except ValueError as error:
print(f"saw value error: {error}")
raiseUse:
with show_error():
raise ValueError("bad")The generator catches the exception, prints, and re-raises.
If a generator context manager catches an exception and does not re-raise it, the exception may be suppressed.
That can be surprising.
So write exception handling carefully.
If you only need cleanup, prefer:
try:
yield
finally:
cleanup()This does not suppress exceptions.
contextlib.suppress intentionally suppresses specified exceptions.
Example:
from contextlib import suppress
with suppress(FileNotFoundError):
Path("missing.txt").unlink()This says:
ignore FileNotFoundError in this block
This is better than a vague bare except.
It names the suppression at the block level.
Use it sparingly.
Good use:
ignore a specific harmless missing-file condition
Bad use:
with suppress(Exception):
risky_operation()That can hide real bugs.
Suppression should be narrow and intentional.
Some objects have a close() method but do not implement context management.
contextlib.closing adapts them:
from contextlib import closing
with closing(resource) as value:
use(value)At exit, it calls:
value.close()This is useful for older APIs or third-party objects that follow a close pattern without supporting with.
If you control the class, it is usually better to implement __enter__ and __exit__ directly.
But closing is a practical adapter.
nullcontext is a context manager that does almost nothing.
It is useful when code sometimes needs a real context manager and sometimes does not.
Example:
from contextlib import nullcontext
def read_text(source):
if isinstance(source, str):
manager = open(source)
else:
manager = nullcontext(source)
with manager as file:
return file.read()If source is a path, open it.
If source is already a file-like object, use it as-is.
nullcontext(value) returns value from __enter__.
This avoids awkward branching around the whole block.
ExitStack handles dynamic numbers of context managers.
Example:
from contextlib import ExitStack
def read_all(paths):
with ExitStack() as stack:
files = [
stack.enter_context(open(path))
for path in paths
]
return [file.read() for file in files]Why not write:
with open(a) as first, open(b) as second:
...Because the number of paths may be dynamic.
ExitStack lets you enter context managers programmatically.
It exits them in reverse order.
It is useful for:
- dynamic resource lists
- optional contexts
- complex setup where partial failure must clean up
- framework code
Do not use ExitStack for simple fixed cases.
Use normal with when you can.
Some context manager helpers can also act as decorators.
For example, context managers built with ContextDecorator behavior can wrap an entire function.
Conceptually:
@some_context()
def function():
...can mean:
def function():
with some_context():
...This is useful for cross-cutting concerns like timing or temporary settings.
But be careful.
Decorators apply to the whole function.
Context managers show the exact block.
Prefer the with statement when a smaller block is clearer.
Transaction context managers deserve special care.
Simplified:
class Transaction:
def __enter__(self):
self.begin()
return self
def __exit__(self, exc_type, exc, traceback):
if exc_type is None:
self.commit()
else:
self.rollback()
return FalseThis means:
success -> commit
exception -> rollback and propagate error
That is usually good.
But real transactions may have subtleties:
- nested transactions
- savepoints
- connection pooling
- retry behavior
- specific exception classes
- commit failures
- rollback failures
Context managers provide the shape.
They do not remove domain complexity.
Design transactional behavior explicitly.
Tests often use context managers.
Examples:
with pytest.raises(ValueError):
Product(-1)This context manager expects an exception.
If the exception happens, it suppresses it and lets the test pass.
If the exception does not happen, the test fails.
Another example:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "data.txt"
path.write_text("hello")The temporary directory is cleaned up after the block.
Tests benefit from context managers because test setup must be cleaned up reliably.
Global state changes should be temporary and well-scoped.
Example:
class ChangeDirectory:
def __init__(self, path):
self.path = path
def __enter__(self):
self.old_path = Path.cwd()
os.chdir(self.path)
return Path.cwd()
def __exit__(self, exc_type, exc, traceback):
os.chdir(self.old_path)Use:
with ChangeDirectory(project_dir):
run_build()The working directory is restored even if run_build() fails.
This is much safer than:
os.chdir(project_dir)
run_build()
os.chdir(old_dir)because the last line can be skipped by exceptions.
Global state changes should almost always use try/finally or a context manager.
Some context manager objects are one-use.
Some can be reused.
Example:
manager = open("data.txt")You should not generally enter the same file object multiple times after it has closed.
Generator-based context managers created by @contextmanager are also one-shot objects.
This is wrong:
manager = announce()
with manager:
...
with manager:
...Create a fresh manager:
with announce():
...
with announce():
...If you design class-based context managers, decide whether instances are reusable.
If not, document that or make misuse fail clearly.
A reentrant context manager can be entered multiple times, even nested.
Example concept:
with manager:
with manager:
...This requires careful state management.
Locks have reentrant and non-reentrant variants.
A normal lock may block or fail if entered twice by the same thread.
A reentrant lock tracks entry count.
For your own context managers, do not assume reentrancy.
If nested use should work, design for it.
If nested use should not work, fail clearly.
Stateful context managers are easy to get wrong when reentered accidentally.
Python also has asynchronous context managers.
They use:
async withand methods:
__aenter__
__aexit__Example shape:
async with client.session() as session:
...This chapter focuses on synchronous context managers.
Async context managers belong with asyncio and asynchronous programming later in the book.
The conceptual parallel is:
with -> __enter__ / __exit__
async with -> __aenter__ / __aexit__
The async version allows setup and cleanup to await asynchronous operations.
This context manager is awkward:
class OpenFile:
def __init__(self, path):
self.path = path
def __enter__(self):
self.file = open(self.path)
def __exit__(self, exc_type, exc, traceback):
self.file.close()Use:
with OpenFile("data.txt") as file:
file.read()file is None because __enter__ returned nothing.
Fix:
def __enter__(self):
self.file = open(self.path)
return self.fileReturn the object the block should use.
That may be self.
It may be an internal resource.
But return it intentionally.
This suppresses every exception:
def __exit__(self, exc_type, exc, traceback):
cleanup()
return TrueThat is dangerous.
The block can fail and the caller may never know.
Usually:
def __exit__(self, exc_type, exc, traceback):
cleanup()
return Falseor:
def __exit__(self, exc_type, exc, traceback):
cleanup()Only return True when suppression is the explicit purpose.
Names like suppress, ignore_missing, or expect_error make suppression visible.
If cleanup raises an exception, it can obscure the original exception.
Example:
def __exit__(self, exc_type, exc, traceback):
self.cleanup_that_may_fail()If the block raised ValueError and cleanup raises RuntimeError, debugging becomes harder.
Sometimes cleanup failure must propagate.
Sometimes it should be logged while preserving the original error.
Design carefully.
For critical resources, cleanup errors matter.
For best-effort cleanup, you may need narrow exception handling.
Avoid broad silent suppression.
__enter__ can raise.
If setup has multiple steps, partial cleanup is your responsibility.
Example:
def __enter__(self):
self.first = acquire_first()
self.second = acquire_second()
return selfIf acquire_second() fails, __exit__ is not called.
So first must be cleaned up inside __enter__:
def __enter__(self):
self.first = acquire_first()
try:
self.second = acquire_second()
except Exception:
release_first(self.first)
raise
return selfFor complex dynamic setup, ExitStack can help.
The guarantee is:
if __enter__ succeeds, __exit__ runs
It is not:
__exit__ always runs no matter how __enter__ fails
This is clear:
with open(path) as file:
data = file.read()This may be unclear:
with user:
...What does entering a user mean?
Does it log in?
Lock the user?
Start a transaction?
Change permissions?
Context managers should make scope meaningful.
Good names help:
with user.impersonation():
...or:
with locked(user):
...If the meaning of entering and exiting is not obvious, use a named method or helper that explains it.
Do not use with just to look sophisticated.
This is unnecessary:
with CalculateTotal(order) as total:
print(total)If there is no setup/cleanup boundary, use a function:
total = calculate_total(order)Context managers are for scoped lifecycle.
They are not a replacement for every helper function.
Ask:
what must be true before the block?
what must be restored or cleaned up after the block?
If there is no good answer, a context manager may not be the right abstraction.
This can surprise users:
with RemoteSession() as session:
...if __enter__ performs slow network setup.
It may still be a good design.
But document it.
Context manager entry can do real work.
Users should understand whether entering:
- opens a file
- connects to a server
- starts a transaction
- acquires a lock
- changes global state
- allocates a large resource
The with statement makes scope clear.
It does not make expensive work obvious by itself.
Good names and documentation matter.
import os
class temporary_env:
def __init__(self, name, value):
self.name = name
self.value = value
def __enter__(self):
self.old_exists = self.name in os.environ
self.old_value = os.environ.get(self.name)
os.environ[self.name] = self.value
return self
def __exit__(self, exc_type, exc, traceback):
if self.old_exists:
os.environ[self.name] = self.old_value
else:
os.environ.pop(self.name, None)Use:
with temporary_env("APP_MODE", "test"):
run_tests()After the block, the environment is restored.
This is better than setting the variable manually and hoping every exit path restores it.
from io import StringIO
import sys
class capture_stdout:
def __enter__(self):
self.old_stdout = sys.stdout
self.buffer = StringIO()
sys.stdout = self.buffer
return self.buffer
def __exit__(self, exc_type, exc, traceback):
sys.stdout = self.old_stdoutUse:
with capture_stdout() as output:
print("hello")
assert output.getvalue() == "hello\n"This is useful in tests.
But it changes global state.
The context manager makes the change temporary.
Still, be careful in threaded programs where global output is shared.
from time import perf_counter
class log_time:
def __init__(self, label):
self.label = label
def __enter__(self):
self.start = perf_counter()
return self
def __exit__(self, exc_type, exc, traceback):
elapsed = perf_counter() - self.start
print(f"{self.label}: {elapsed:.3f}s")Use:
with log_time("build index"):
build_index()This does not suppress exceptions.
If build_index() fails, the time is still logged and the exception continues.
That is usually the right behavior for timing.
Class version:
class temporary_value:
def __init__(self, obj, name, value):
self.obj = obj
self.name = name
self.value = value
def __enter__(self):
self.old_value = getattr(self.obj, self.name)
setattr(self.obj, self.name, self.value)
def __exit__(self, exc_type, exc, traceback):
setattr(self.obj, self.name, self.old_value)Generator version:
from contextlib import contextmanager
@contextmanager
def temporary_value(obj, name, value):
old_value = getattr(obj, name)
setattr(obj, name, value)
try:
yield
finally:
setattr(obj, name, old_value)Use:
with temporary_value(settings, "debug", True):
run_debug_code()The generator version is concise.
The class version may be better if you need:
- reusable manager objects
- several methods
- richer state inspection
- inheritance
- more explicit control
Both are valid.
Use a class-based context manager when:
- the manager has meaningful state
- the manager needs multiple methods
- the manager may be reused carefully
- inheritance or composition matters
- setup and cleanup are complex
- you want explicit protocol methods
Use @contextmanager when:
- setup is simple
- cleanup is simple
- one
yieldexpresses the resource boundary - a small helper would be clearer than a full class
Example good generator context manager:
@contextmanager
def changed_directory(path):
old = Path.cwd()
os.chdir(path)
try:
yield
finally:
os.chdir(old)Example good class context manager:
class Transaction:
...where transaction state and methods matter.
Before writing a context manager, ask:
What is acquired before the block?
If nothing, maybe you need a function.
Ask:
What must be released, restored, committed, or rolled back after the block?
That is the cleanup responsibility.
Ask:
What should the as target receive?
Return that from __enter__ or yield it from @contextmanager.
Ask:
Should exceptions be suppressed?
Usually no.
Ask:
Can setup fail halfway?
If yes, handle partial cleanup in __enter__.
Ask:
Is this manager reusable or one-shot?
Document or enforce the answer.
Ask:
Would contextlib already solve this?
Check suppress, closing, nullcontext, ExitStack, and contextmanager.
Write a context manager that prints before and after a block.
Solution:
class announce:
def __enter__(self):
print("start")
return self
def __exit__(self, exc_type, exc, traceback):
print("end")Use:
with announce():
print("inside")Expected:
start
inside
end
Write a timer context manager.
Solution:
from time import perf_counter
class Timer:
def __enter__(self):
self.start = perf_counter()
return self
def __exit__(self, exc_type, exc, traceback):
self.end = perf_counter()
self.elapsed = self.end - self.startUse:
with Timer() as timer:
sum(range(1000))
assert timer.elapsed >= 0The as target receives the timer object because __enter__ returns self.
Write a context manager that suppresses FileNotFoundError.
Solution:
class suppress_file_not_found:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, traceback):
return exc_type is FileNotFoundErrorUse:
with suppress_file_not_found():
Path("missing.txt").unlink()In real code, prefer:
from contextlib import suppress
with suppress(FileNotFoundError):
Path("missing.txt").unlink()The standard-library version is clearer.
Write a generator-based context manager that temporarily sets an attribute.
Solution:
from contextlib import contextmanager
@contextmanager
def temporary_attr(obj, name, value):
old_value = getattr(obj, name)
setattr(obj, name, value)
try:
yield
finally:
setattr(obj, name, old_value)Use:
with temporary_attr(settings, "debug", True):
assert settings.debug is TrueAfter the block, the old value is restored.
Use multiple context managers to copy text from one file to another.
Solution:
with open("input.txt") as source, open("output.txt", "w") as target:
target.write(source.read())Equivalent nested form:
with open("input.txt") as source:
with open("output.txt", "w") as target:
target.write(source.read())Entering happens left to right.
Exiting happens right to left.
What is wrong?
class Manager:
def __enter__(self):
self.resource = acquire()
def __exit__(self, exc_type, exc, traceback):
self.resource.close()Answer:
__enter__ does not return the resource or self.
So:
with Manager() as resource:
...assigns None to resource.
Fix:
def __enter__(self):
self.resource = acquire()
return self.resourceor:
return selfdepending on what the block should use.
Decide whether each fits a context manager:
Open and close a file
Calculate a total
Acquire and release a lock
Temporarily change working directory
Parse a date string
Begin and commit/rollback a transaction
Measure time for a block
Validate an email address
Likely answers:
Open and close a file -> yes
Calculate a total -> no, use function
Acquire and release a lock -> yes
Temporarily change working directory -> yes
Parse a date string -> no, use function/classmethod
Transaction -> yes
Measure time for a block -> yes
Validate email -> no, use function/property/validator
The pattern:
scoped lifecycle -> context manager
plain computation -> function or method
Context managers package setup and cleanup around a block of code.
The with statement uses the context manager protocol.
A context manager implements __enter__ and __exit__.
__enter__ runs before the block.
Its return value is assigned to the name after as.
__exit__ runs after the block if __enter__ succeeded.
If the block raised an exception, __exit__ receives exception information.
If the block exited normally, __exit__ receives three None values.
Returning a true value from __exit__ suppresses the exception.
Most context managers should not suppress exceptions.
Files, locks, transactions, timers, temporary settings, and test helpers are natural context managers.
contextlib.contextmanager lets a generator function define a context manager with setup before yield and cleanup after yield.
contextlib.suppress, closing, nullcontext, and ExitStack provide useful standard tools.
The design principle is:
use context managers for scoped lifecycle, not ordinary computation
When a block needs a guarantee that something will be cleaned up, restored, released, committed, rolled back, or measured, a context manager may be the right abstraction.
Chapter 60 studied context managers as a Pythonic abstraction for scoped setup and cleanup.
Next we study decorators.
Decorators modify or wrap functions, methods, and classes.
They are used for:
- logging
- timing
- caching
- validation
- registration
- permissions
- retries
- framework routes
- test markers
- class transformation
Chapter 61 will explain:
- what decorator syntax means
- how functions can wrap other functions
- why closures matter for decorators
- how to preserve metadata with
functools.wraps - how decorators with arguments work
- how class decorators work
- when decorators improve design
- when decorators hide too much
The transition is:
context managers wrap a block of execution
decorators wrap callable or class definitions
Both abstractions let us factor repeated structure out of business logic.
Used carefully, they make code cleaner.
Used carelessly, they make behavior harder to see.