Trace inside the firstIn
trace_cm.py, nest a secondwith Trace("B") as u:block inside the body of the firstwith Trace("A") as t:block, with its ownprint(f"inside {u.name}"). Before running it, predict the order in which the six “enter”/“inside”/“exit” lines appear.
The
Protocol shows __enter__() running when the
with begins and __exit__() when its
body ends. A second with inside the body is a
complete enter-and-exit pair that sits within the first. Write
out the six lines by following which block must finish before
the other can.
# The shape of exercise_1.py
from typing import Self
class Trace:
def __init__(self, name: str) -> None:
...
def __enter__(self) -> Self:
...
def __exit__(self, exc_type: type[BaseException] | None,
exc: object, tb: object) -> None:
...# exercise_1.py
from typing import Self
class Trace:
def __init__(self, name: str) -> None:
self.name = name
def __enter__(self) -> Self:
print(f"enter {self.name}")
return self
def __exit__(self, exc_type: type[BaseException] | None,
exc: object, tb: object) -> None:
print(f"exit {self.name}")
with Trace("A") as t:
print(f"inside {t.name}")
with Trace("B") as u:
print(f"inside {u.name}")
#: enter A
#: inside A
#: enter B
#: inside B
#: exit B
#: exit AEntering is outside-in (A then B),
and exiting is inside-out (B then A).
B’s whole lifetime, enter and exit, sits nested
inside A’s, the same last-in-first-out order Combining
Context Managers shows for tag("ul") and
tag("li") written on one with line.
The order is the same whether you write the nesting as two
separate with statements or as one comma-separated
line.
In
demo_exceptions.py, changeexpected(ZeroDivisionError)toexpected((ZeroDivisionError, TypeError)), then raise aTypeErrorinstead of dividing by zero, and confirm thatexpectedcatches and prints it too.
In The
expected Manager, __exit__()
decides what to suppress by testing the exception type against
the manager’s types argument. Look at which kinds
of value issubclass() accepts as its second
argument. Then pass a tuple of classes at the call site and
watch for the extra pair of parentheses.
# ch15_expected_types.py
from exceptions import expected
with expected((ZeroDivisionError, TypeError)):
print("before")
raise TypeError("not a number")
#: before
#: [TypeError] not a number
print("survived")
#: survived
with expected((ZeroDivisionError, TypeError)):
print("before")
1 / 0
#: before
#: [ZeroDivisionError] division by zero
print("survived")
#: survivedWiden what the manager catches. You make
every change the exercise asks for at the call site.
expected takes one types argument that
is either an exception class or a tuple of them, and
issubclass(exc_type, self.types) accepts either
shape. Passing (ZeroDivisionError, TypeError)
therefore suppresses both, and the TypeError block
prints a [Type] message line in the same form the
ZeroDivisionError block printed before the
change.
Check that the original type still matches. The second block shows that the tuple still covers the original exception.
Note the double parentheses.
expected((ZeroDivisionError, TypeError)) passes one
argument, a tuple.
expected(ZeroDivisionError, TypeError) passes two,
and Python raises a TypeError at the call, since
expected declares a single parameter. A version
taking *types accepts the second form, and that is
the design contextlib.suppress chose.
with lineAdd a third manager to the
withstatement inmultiple.py,tag("li")again for a second item, and confirm the exit order still reverses the entry order.
Combining
Context Managers shows several managers on one
with statement, entered left to right. Add another
tag("li") with its own as name to the
parenthesized list. Predict the closing tags before you run it,
using the same last-in-first-out rule.
# The shape of exercise_3.py
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def tag(name: str) -> Iterator[str]:
...# exercise_3.py
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def tag(name: str) -> Iterator[str]:
print(f"<{name}>")
try:
yield name
finally:
print(f"</{name}>")
with (tag("ul") as outer, tag("li") as inner1,
tag("li") as inner2):
print(f" {outer} then {inner1} then {inner2}")
#: <ul>
#: <li>
#: <li>
#: ul then li then li
#: </li>
#: </li>
#: </ul>All three managers enter left to right (ul, then
li, then li again) and exit in reverse
order, regardless of how many managers appear on the line. The
pattern from two managers extends unchanged to three, four, or
more.
In
test_object_pool.py, add a test that leases both connections at once, entering a secondwith pool.lease()block inside the first, and confirmspool.available()reaches0.
Testing
the Lease shows a test that leases a connection with
pool.lease() and checks
pool.available(). Nest a second
with pool.lease() inside the first and assert on
available() while the test holds both connections.
Check the count again after both blocks exit.
# test_ch15_both_leased.py
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from queue import Queue
@dataclass(frozen=True)
class Connection:
number: int
class Pool[R]:
def __init__(self, *items: R) -> None:
self._available: Queue[R] = Queue()
for item in items:
self._available.put(item)
@contextmanager
def lease(self) -> Iterator[R]:
item = self._available.get()
try:
yield item
finally:
self._available.put(item)
def available(self) -> int:
return self._available.qsize()
def test_both_leased_at_once() -> None:
pool = Pool(Connection(1), Connection(2))
with pool.lease() as first:
with pool.lease() as second:
assert second is not first
assert pool.available() == 0
assert pool.available() == 2The solutions tree cannot import the chapter’s object_pool.py, so the
file carries its own copy of Connection and
Pool. In the chapter’s test_object_pool.py you
add the test function alone.
Hold both connections at once. The first
lease() takes one connection out of the queue, and
the nested second lease() takes the other, so
pool.available() is 0 inside the inner
with. The 0 confirms the pool has no
built-in limit of “one lease at a time.” The pool holds the
items you gave its constructor, and it hands out as many
concurrent leases as it has items. A third nested
lease() would block forever in get(),
since this one thread holds both connections and nothing can
return one.
Check that both connections come back.
Exiting the inner with puts second
back, then exiting the outer with puts
first back, restoring pool.available()
to 2.
banner decorators stacked on one functionStack
@banner("outer")and@banner("inner")fromcontext_decorator.pyon a single function and predict the order of the four bracketing lines before running it.
Context
Manager as Decorator shows a @contextmanager
generator used as a decorator through
ContextDecorator. Stacked decorators apply from the
one nearest the def outward, so work out which
manager wraps which. Each call of the function then enters the
managers in the order of the wrapping.
# The shape of exercise_5.py
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def banner(title: str) -> Iterator[None]:
...
@banner("outer")
@banner("inner")
def report() -> None:
...# exercise_5.py
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def banner(title: str) -> Iterator[None]:
print(f"=== {title} ===")
try:
yield
finally:
print(f"=== {title} ends ===")
@banner("outer")
@banner("inner")
def report() -> None:
print("quarterly numbers")
report()
#: === outer ===
#: === inner ===
#: quarterly numbers
#: === inner ends ===
#: === outer ends ===Apply the decorators bottom-up. The
prediction is the same one stacking produces anywhere. Python
reads the stack as
report = banner("outer")(banner("inner")(report)).
@banner("inner") is nearest the def,
so it wraps report() first, and
@banner("outer") then wraps the inner wrapper.
Calling report() therefore enters the outer
manager, which calls the inner wrapper, which enters the inner
manager before running the body. Unwinding reverses that order,
so the four bracketing lines nest rather than interleave.
Enter a fresh manager on each call. Each
@banner(...) line builds one manager object, when
Python defines report(). A generator manager is
single-use, so the wrapper that ContextDecorator
supplies does not enter that object. On each call of
report() the wrapper builds a fresh manager from
the same generator function and arguments, and enters that one.
A hand-written class-based manager decorating a function
re-enters the same instance on every call instead, so every call
shares any state the instance holds.
ignore_missing, which suppresses only
KeyErrorWrite a context manager
ignore_missingwhose__exit__()suppressesKeyErrorand lets everything else through, without usingcontextlib.suppress. Test it with a block that raises aKeyErrorand a block that raises aValueError.
The
__exit__() Arguments shows that a true return
value from __exit__() suppresses the exception and
a false one lets it continue. Write a class whose
__exit__() returns the result of testing
exc_type against KeyError.
exc_type is None when the block raises
nothing. Test the ValueError case inside the
chapter’s expected() so it catches and prints the
exception.
# The shape of ignore_missing.py
from types import TracebackType
from exceptions import expected
class ignore_missing:
def __enter__(self) -> None:
...
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
...If you return issubclass(exc_type, KeyError)
without the None test, the demo prints the same two
lines, because both of its blocks raise an exception. A block
that finishes cleanly then fails: __exit__()
receives None, and issubclass() raises
a TypeError. ty reports an
invalid-argument-type at the
issubclass() call, so the type checker catches the
mistake the demo misses. The solution tests
exc_type is not None first.
# ignore_missing.py
from types import TracebackType
from exceptions import expected
class ignore_missing:
def __enter__(self) -> None:
return None
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
return (exc_type is not None
and issubclass(exc_type, KeyError))
stock = {"apple": 3}
with ignore_missing():
print(stock["pear"])
print("never reached")
print("survived the KeyError")
#: survived the KeyError
with expected(ValueError):
with ignore_missing():
raise ValueError("not a lookup problem")
#: [ValueError] not a lookup problemSuppress one exception type.
__exit__() decides an exception’s fate through its
return value: truthy suppresses, falsy lets the exception
continue. Returning issubclass(exc_type, KeyError)
therefore suppresses KeyError and propagates
everything else. The second block confirms the propagation: the
ValueError passes through
ignore_missing and reaches the chapter’s
expected, which prints it.
Handle a clean exit. The
exc_type is not None test keeps the normal path
working. When a block finishes without an exception, Python
still calls __exit__(), passing None
for all three arguments, and
issubclass(None, KeyError) raises a
TypeError. Checking for None first
also lets the type checker narrow exc_type to
type[BaseException], the type
issubclass() requires.
The class uses a lowercase name because you use it like a
function. contextlib.suppress is lowercase for the
same reason.
exit_stack.py driven from
the command lineRewrite
exit_stack.pyto take its names fromsys.argv[1:], run it with no arguments and with three, and confirm the close order reverses the open order in both cases.
Combining
Context Managers shows ExitStack entering a
number of managers that the code decides at runtime. Build the
list of names from sys.argv[1:] and pass it to the
function that calls enter_context() for each one.
Run it with no names and with three, and compare the close order
with the open order.
# The shape of exercise_7.py
from collections.abc import Iterator
from contextlib import ExitStack, contextmanager
@contextmanager
def tag(name: str) -> Iterator[str]:
...
def wrap(names: list[str]) -> None:
...Both calls to wrap() go, replaced by one call
that reads the names from the command line:
import sys
wrap(sys.argv[1:])Run with three names,
uv run python exit_stack.py x y z:
open x
open y
open z
using ['x', 'y', 'z']
close z
close y
close x
Run with none, uv run python exit_stack.py:
using []
Close in reverse order of opening.
enter_context() pushes each manager onto the stack
as the comprehension walks the list left to right. Leaving the
with unwinds that stack, so the closes come out in
reverse. The reversal holds for any number of names, including
zero, the property the exercise asks you to confirm.
The empty run is the more interesting one. Nothing opens, so
nothing closes, and with ExitStack() as stack:
still enters and exits correctly around a body whose stack stays
empty. That degenerate case shows why ExitStack
exists. A fixed with a, b, c: line settles its
count in the source. ExitStack accepts a count
settled only at runtime, zero included, and a command line is
one source of such a count.
The sys.argv rewrite stays out of the extracted
listings, because the book’s output checker runs every listing
inside one process with its own arguments, and a script reading
sys.argv sees the checker’s arguments instead. The
listing below passes the names to wrap() directly,
so the checker can run it, and it shows both cases:
# exercise_7.py
from collections.abc import Iterator
from contextlib import ExitStack, contextmanager
@contextmanager
def tag(name: str) -> Iterator[str]:
print(f"open {name}")
try:
yield name
finally:
print(f"close {name}")
def wrap(names: list[str]) -> None:
with ExitStack() as stack:
open_tags = [stack.enter_context(tag(n))
for n in names]
print("using", open_tags)
wrap([])
#: using []
wrap(["x", "y", "z"])
#: open x
#: open y
#: open z
#: using ['x', 'y', 'z']
#: close z
#: close y
#: close xcareless() with its
try/finally restoredWrap the
yieldincareless()fromno_finally.pyintry/finally, with theexitline in thefinally. Before running it, predict whereexit Aappears relative tocaught: boom.
A
Basic Context Manager shows that a
@contextmanager generator needs
try/finally around its
yield to clean up after an exception. The
with statement throws the block’s exception into
the generator at the yield. Decide whether the
finally body runs before or after the
except clause outside the with.
# The shape of exercise_8.py
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def careful(name: str) -> Iterator[str]:
...# exercise_8.py
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def careful(name: str) -> Iterator[str]:
print(f"enter {name}")
try:
yield name
finally:
print(f"exit {name}")
try:
with careful("A"):
raise ValueError("boom")
except ValueError as error:
print("caught:", error)
#: enter A
#: exit A
#: caught: boomClean up before the exception propagates.
exit A now prints, and it prints before
caught: boom. Python raises the block’s
ValueError inside the generator, at the
yield. The finally runs as the
exception leaves the generator, and only then does the exception
leave the with statement and reach the
except. That is the order exit_on_error.py shows for
the class form: cleanup first, then propagation.
In no_finally.py the same
exception left the generator from the bare yield,
so the print() after it did not run. The
finally is the only difference between the two
listings, apart from the function’s name.