Warm-up · Activity 1 of 7
// A1.4 · ~30 min · Advanced
Writing context managers
After this lesson you can write a class that works in a with statement, cleans up on every exit path and suppresses exactly the exceptions you choose.
Predict · Activity 2 of 7
Predict before you read on: what does this print?
class Tag: def __init__(self, name): self.name = name def __enter__(self): print(f"<{self.name}>", end=" ") return self def __exit__(self, exc_type, exc, tb): print(f"</{self.name}>") with Tag("p") as t: print("hello from", t.name, end=" ")Practice · Activity 3 of 7
Fill in the method name so that with Session() as s: binds s to "cursor-1".
class Session: def ____(self): return "cursor-1" def __exit__(self, exc_type, exc, tb): passdef (self):Practice · Activity 4 of 7
The block ends normally. What does __exit__ print?
class Probe: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): print(exc_type, exc, tb) with Probe(): passPractice · Activity 5 of 7
Match each situation to what happens.
Brain teaser · Activity 6 of 7
Brain teaser. Quiet suppresses every exception. What does load() return?
class Quiet: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return True def load(): with Quiet(): value = int("x") return value return -1 print(load())Apply · Activity 7 of 7
Mini-task. Write a class Indenter whose object can be used in nested with statements. Each with raises the level by one on entry and lowers it on exit, and a method print(text) prints the text after as many dashes as the current level.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A transaction on a dictionary
Transaction hands the block a draft copy through as. If the block finishes, __exit__ copies the draft back: committed. If the block raises, __exit__ throws the draft away and returns None, so the caller still sees the error and the stock is unchanged. Try raising in the first block, or make __exit__ return True.
main.py
from types import TracebackType
class Transaction:
"""Changes a copy of the data; commits it only if the block succeeds."""
def __init__(self, data: dict[str, int]) -> None:
self.data = data
def __enter__(self) -> dict[str, int]:
self.draft = dict(self.data)
return self.draft
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
if exc_type is None:
self.data.clear()
self.data.update(self.draft)
print("committed")
else:
# Returning None (false) lets the exception continue.
print(f"rolled back after {exc_type.__name__}: {exc}")
stock = {"apples": 5, "pears": 2}
with Transaction(stock) as draft:
draft["apples"] -= 3
print(stock)
try:
with Transaction(stock) as draft:
draft["apples"] -= 1
draft["pears"] -= 5
if draft["pears"] < 0:
raise ValueError("not enough pears")
except ValueError:
print("the caller still sees the ValueError")
print(stock)
Run it with
python main.pyOutput
committed
{'apples': 2, 'pears': 2}
rolled back after ValueError: not enough pears
the caller still sees the ValueError
{'apples': 2, 'pears': 2}- as draft binds the copy returned by __enter__, not the Transaction.
- The second block changed apples in the draft too, and that change was thrown away.
- __exit__ reads exc_type.__name__ and exc to report what went wrong.
- Because __exit__ returns None, the ValueError reaches the try around the with.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 2
Tags that always close
Tag(name, out) writes lines into the list out. Add __enter__, which appends "<name>" and returns self, and __exit__, which appends "</name>". The closing line must also be written when the block raises, and the exception must still reach the caller.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
__exit__ takes four parameters: self, exc_type, exc and tb.
Hint 2
Python calls __exit__ after an exception too, so appending there is enough; no try is needed.
Hint 3
Let __exit__ return nothing: None is false, so the exception is not suppressed.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from types import TracebackType
class Tag:
"""Writes an opening line on entry and the closing line on exit."""
def __init__(self, name: str, out: list[str]) -> None:
self.name = name
self.out = out
def __enter__(self) -> "Tag":
self.out.append(f"<{self.name}>")
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
self.out.append(f"</{self.name}>")
if __name__ == "__main__":
lines: list[str] = []
with Tag("ul", lines):
with Tag("li", lines) as item:
item.out.append("tea")
print(lines)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from types import TracebackType
class Tag:
"""Writes an opening line on entry and the closing line on exit."""
def __init__(self, name: str, out: list[str]) -> None:
self.name = name
self.out = out
# Add __enter__: append "<name>" to self.out and return self.
# Add __exit__: append "</name>", even when the block raised,
# and do not suppress the exception.
if __name__ == "__main__":
lines: list[str] = []
with Tag("ul", lines):
with Tag("li", lines) as item:
item.out.append("tea")
print(lines)
test_main.py
from main import Tag
def test_as_gives_the_tag():
"""with Tag(...) as t binds the Tag itself"""
out = []
tag = Tag("p", out)
with tag as t:
assert t is tag, f"the as target is {t!r}, expected the Tag: return self from __enter__"
def test_nested_lines():
"""Nested tags open and close in the right order"""
out = []
with Tag("ul", out):
with Tag("li", out):
out.append("tea")
assert out == ["<ul>", "<li>", "tea", "</li>", "</ul>"], f"the lines are {out!r}"
def test_closes_on_error():
"""The closing line is written even when the block raises"""
out = []
try:
with Tag("p", out):
raise ValueError("boom")
except ValueError:
pass
assert out == ["<p>", "</p>"], f"after an error the lines are {out!r}, expected ['<p>', '</p>']"
def test_does_not_suppress():
"""An exception in the block still reaches the caller"""
try:
with Tag("p", []):
raise ValueError("boom")
except ValueError:
return
raise AssertionError("the ValueError was swallowed: __exit__ must not return True")
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
A selective Ignore
Finish __exit__ in Ignore. It suppresses an exception only if it is an instance of one of self.types (subclasses count), and then stores the exception object in self.caught. Every other exception must reach the caller, and a clean block leaves caught as None.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
On a clean exit exc_type is None, and issubclass(None, ...) would fail, so check that first.
Hint 2
issubclass accepts a tuple of classes: issubclass(exc_type, self.types).
Hint 3
if exc_type is not None and issubclass(exc_type, self.types): self.caught = exc; return True. Otherwise return False.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from types import TracebackType
class Ignore:
"""Suppresses the given exception types and remembers what it caught."""
def __init__(self, *types: type[BaseException]) -> None:
self.types = types
self.caught: BaseException | None = None
def __enter__(self) -> "Ignore":
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
if exc_type is not None and issubclass(exc_type, self.types):
self.caught = exc
return True
return False
if __name__ == "__main__":
prices: dict[str, int] = {}
with Ignore(KeyError) as guard:
prices["tea"]
print("caught:", repr(guard.caught))
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from types import TracebackType
class Ignore:
"""Suppresses the given exception types and remembers what it caught."""
def __init__(self, *types: type[BaseException]) -> None:
self.types = types
self.caught: BaseException | None = None
def __enter__(self) -> "Ignore":
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
# Suppress (return True) only exceptions of self.types or their
# subclasses, and store the exception in self.caught.
return False
if __name__ == "__main__":
prices: dict[str, int] = {}
with Ignore(KeyError) as guard:
prices["tea"]
print("caught:", repr(guard.caught))
test_main.py
from main import Ignore
def test_suppresses_listed_type():
"""A listed exception is suppressed and stored"""
with Ignore(ValueError) as guard:
raise ValueError("bad number")
assert isinstance(guard.caught, ValueError), f"guard.caught is {guard.caught!r}, expected the ValueError"
def test_subclasses_count():
"""Ignore(LookupError) also suppresses KeyError"""
with Ignore(LookupError) as guard:
{}["missing"]
assert isinstance(guard.caught, KeyError), f"guard.caught is {guard.caught!r}, expected a KeyError"
def test_other_types_propagate():
"""An exception that is not listed reaches the caller"""
try:
with Ignore(ValueError):
raise KeyError("id")
except KeyError:
return
raise AssertionError("Ignore(ValueError) swallowed a KeyError")
def test_clean_block():
"""Without an exception, caught stays None"""
with Ignore(ValueError) as guard:
pass
assert guard.caught is None, f"guard.caught is {guard.caught!r} after a clean block"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
__enter__ without return
class LogFile:
def __init__(self):
self.lines = []
def __enter__(self):
self.lines.append("start")
def __exit__(self, exc_type, exc, tb):
self.lines.append("end")
with LogFile() as log:
log.lines.append("working")
What Python prints
AttributeError: 'NoneType' object has no attribute 'lines'Why, and the fix
The name after as gets whatever __enter__ returns, and a function without return returns None. End __enter__ with return self, or return the resource the block should use.
__exit__ with the wrong parameters
class Tag:
def __enter__(self):
return self
def __exit__(self):
print("closing")
with Tag():
print("inside")
What Python prints
TypeError: Tag.__exit__() takes 1 positional argument but 4 were givenWhy, and the fix
Python always passes three arguments to __exit__: exc_type, exc and tb, which are None after a clean block. Write def __exit__(self, exc_type, exc, tb):. The error comes after the block has already run.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.