Writing context managers, and chaining exceptions
The __enter__/__exit__ protocol, how __exit__ can swallow an exception, the try/finally that @contextmanager needs, and raise ... from.
After this lesson you can
- Write a context manager as a class and as a @contextmanager generator
- Say what __exit__ receives and what its return value decides
- Explain why a @contextmanager generator needs try/finally around yield
- Chain exceptions with raise ... from and read __cause__ and __context__
Lesson 5 used with on files. Any object can take part, through two
methods:
__enter__(self)runs when the block starts; whatever it returns is bound to the name afteras.__exit__(self, exc_type, exc, tb)runs when the block ends, however it ends. After a normal finish all three arguments areNone. After an exception they describe it, and the return value decides what happens next: a truthy return suppresses the exception, anything else (including the implicitNone) lets it continue outwards.
with a, b: enters a then b, and exits in reverse: b first. That
is the order you want — what was acquired last is released first.
log = [] class Step: def __init__(self, name, swallow=False): self.name = name self.swallow = swallow def __enter__(self): log.append(f"enter {self.name}") return self def __exit__(self, exc_type, exc, tb): seen = exc_type.__name__ if exc_type else None log.append(f"exit {self.name} saw {seen}") return self.swallow def run(): log.clear() with Step("a"), Step("b"): log.append("body") try: with Step("plain"): raise KeyError("k") except KeyError: log.append("KeyError reached the caller") with Step("swallowing", swallow=True): raise KeyError("k") log.append("after swallowing: no exception") return logSuppressing is rarely right. Return True only for an exception you
have genuinely handled, and check its type first; the standard library's
contextlib.suppress(FileNotFoundError) is the explicit way to say
"ignore exactly this".
@contextmanager
contextlib.contextmanager turns a generator into a context manager:
the code before yield is the enter step, the yielded value is what
as receives, and the code after it is the exit step. If the block
raises, the exception is thrown into the generator at the yield.
Without a try/finally around the yield, the cleanup line after it
never runs.
from contextlib import contextmanager
@contextmanager
def opened(resource):
resource.open()
try:
yield resource
finally:
resource.close() # runs on success and on error
from contextlib import contextmanager log = [] @contextmanagerdef careless(name): log.append(f"open {name}") yield name log.append(f"close {name}") # skipped if the block raises @contextmanagerdef careful(name): log.append(f"open {name}") try: yield name finally: log.append(f"close {name}") def run(): log.clear() for manager in (careless, careful): try: with manager(manager.__name__): raise RuntimeError("boom") except RuntimeError: log.append("caught boom") return logChaining exceptions
When you translate a low-level error into one that fits your API, keep
the original. raise ConfigError("bad port") from err stores err as
the new exception's __cause__, and the traceback shows both, joined
by "The above exception was the direct cause of the following
exception". Raising inside an except block without from still
records the original, as __context__ ("During handling of the above
exception, another exception occurred"), which reads like a second bug.
raise ... from None hides the original deliberately.
class ConfigError(Exception): pass def parse_port(text, explicit): try: return int(text) except ValueError as err: if explicit: raise ConfigError(f"bad port {text!r}") from err raise ConfigError(f"bad port {text!r}") def describe(explicit): try: parse_port("80a", explicit) except ConfigError as e: return { "error": str(e), "cause": repr(e.__cause__), "context": repr(e.__context__), "suppress_context": e.__suppress_context__, } def run(): return {"with_from": describe(True), "without_from": describe(False)}Common mistakes
- A
@contextmanagergenerator with notry/finally: the cleanup afteryieldis skipped exactly when an error makes it matter. __exit__that returnsTrueunconditionally, so every exception in the block, including real bugs, disappears.- Re-raising a new exception without
from, so the log shows two unrelated-looking errors instead of a cause and its effect.
What interviewers ask
- What does
__exit__receive, and how can it suppress an exception? The exception's type, value and traceback (allNoneon success); returning a truthy value suppresses it. - How would you write a context manager without a class? A generator
with
@contextmanager, the setup beforeyield, the teardown in afinallyafter it. - What is the difference between
__cause__and__context__?__cause__is set explicitly byraise ... from;__context__is recorded automatically when an exception is raised while another is being handled.
Exercise · Pro
Staged changes that apply all at once or not at all
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade