Challenge 3: The Same Logging Context Manager with contextlib — Possible Solution ==================================================================== from contextlib import contextmanager @contextmanager def log_section(name): print(f"Entering {name}") try: yield finally: print(f"Exiting {name}") with log_section("data processing"): print("doing work...") print("still working...") Output: Entering data processing doing work... still working... Exiting data processing WHY THIS WORKS AS AN ANSWER ------------------------------ @contextmanager above def log_section(name): reuses this chapter's own timer() example structure exactly — turning an ordinary generator function into a full context manager, with none of Challenge 2's __init__/__enter__/__exit__ boilerplate. Code written BEFORE yield — here, print(f"Entering {name}") — becomes the __enter__ logic, running the moment the with block starts, per this chapter's own explanation of how @contextmanager maps generator code onto the two dunder methods. yield itself (with no value, since this challenge's with statement doesn't use as x:) is where the with block's own body actually runs — the two print("...") calls happen at this exact point, reusing the same "the code pauses at yield and resumes later" mechanic from generators (Course 2, Chapter 5). Wrapping yield in try/finally reuses this chapter's own timer() example precisely, and for the same reason: finally guarantees print(f"Exiting {name}") runs even if an exception occurred inside the with block, matching the class-based version's own guarantee from Challenge 2 that __exit__ always runs. This produces identical output to Challenge 2's LogSection class, just written as a much shorter generator function instead of a full class with two separate dunder methods — directly demonstrating the boilerplate trade-off this chapter's own comparison called out.