Decorators & Context Managers
Wrapping a function's behavior without changing its code, and guaranteeing setup/teardown runs even when something fails.
Try answering in your head first, then click a question to check the model answer.
Q1.Walk through what happens when you apply `@my_decorator` above a function definition.(show answer)
It's syntactic sugar for func = my_decorator(func) — Python defines the function, then immediately passes it to my_decorator, and rebinds the name to whatever that returns (typically a wrapper function that calls the original, plus extra behavior before/after).
Q2.Why use a context manager (`with` statement) instead of manual try/finally for resource cleanup?(show answer)
A context manager encapsulates the setup/teardown logic once, in __enter__/__exit__, so callers don't need to repeat try/finally boilerplate at every call site. It's less error-prone — it's easy to forget a finally block, but a with statement always guarantees __exit__ runs, even on an exception.
Q3.How would you write a simple context manager using `contextlib.contextmanager` instead of a class?(show answer)
Decorate a generator function with @contextlib.contextmanager; code before the single yield runs as __enter__, the yielded value (if any) is what as binds to, and code after yield runs as __exit__ (wrap the yield in try/finally to guarantee cleanup runs on an exception).
Q4.Why is forgetting `@functools.wraps` in a decorator a real bug, not just a style nitpick?(show answer)
Without it, the wrapper function replaces the original's __name__, __doc__, and signature with the wrapper's own generic ones — this breaks introspection tools (debuggers, documentation generators, help()), and can silently confuse other decorators or frameworks that rely on the function's real identity/metadata.
