Decorators and context managers
A decorator wraps a function with work before and after it. A context manager wraps a block with setup and guaranteed cleanup. Trace both on a small support tool.
Two patterns show up in almost every Python library, and both are about doing something around your code without changing it.
A decorator is a function that takes a function and returns a new one. The @name line above a def is shorthand for func = name(func). The returned function usually does some extra work (timing, logging, checking permissions, retrying), then calls the original. functools.wraps makes the new function keep the original’s name and docstring, so the wrapping is invisible to tools that read them.
A context manager is an object that a with statement can enter and exit. The exit code runs when the block ends, even if the block raised an exception. Files, locks and database connections all work this way. contextlib.contextmanager lets you write one as a generator: the code before yield is the setup, the code after it is the cleanup.
Press Run to trace @timed wrapping a slow lookup. Then switch Try it to the with open_ticket(...) block.
# A timing decorator and a context manager for a small support tool.
import functools
import time
from contextlib import contextmanager
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
ms = (time.perf_counter() - start) * 1000
print(f"{func.__name__} took {ms:.1f} ms")
return result
return wrapper
@timed
def get_pincode(city):
"""Look up a pincode (slowly, so the timing is visible)."""
time.sleep(0.05)
return {"Chennai": "600001", "Pune": "411001"}[city]
@contextmanager
def open_ticket(ticket_id):
print("open", ticket_id)
try:
yield {"id": ticket_id, "notes": []}
finally:
print("close", ticket_id)
print(get_pincode("Chennai"))
print(get_pincode.__name__, "|", get_pincode.__doc__)
with open_ticket("T-101") as ticket:
ticket["notes"].append("asked for pincode")
print("inside, notes:", ticket["notes"])
print("after the with block")Press Play to run the program line by line, or step with ← →.
Variables
Nothing defined yet.
Output
Nothing printed yet.
What to notice
- Decoration happens at definition time.
@timedruns when Python reads thedef, long before anyone callsget_pincode. After it, the nameget_pincodepoints atwrapper. *args, **kwargspass anything through. The wrapper does not know or care what arguments the original takes. It forwards them all and returns whatever came back.functools.wrapskeeps the identity. Without it,get_pincode.__name__would be'wrapper'and the docstring would be gone. With it, the output shows the original name and docstring.yieldsplits setup from cleanup. Inopen_ticket, everything beforeyieldruns on entering thewith, the yielded value becomesticket, and everything afteryieldruns on exit.finallymakes cleanup unconditional. If the block raised, the exception would be re-raised at theyieldinside the generator, andfinallywould still printclose. That guarantee is the reason to usewithinstead of callingclose()yourself.- Same shape, different scope. A decorator wraps a whole function for every call. A context manager wraps one block, once. Pick by what you want to wrap.
Decorator
Wraps a function. Runs code before and after every call. Applied once with @name.
Context manager
Wraps a block. Runs setup on enter and cleanup on exit, even on error. Used with with.
Why this matters for AI work
ADK’s callbacks are the decorator idea applied to an agent: before_tool_callback and after_tool_callback run around every tool call, before_model_callback and after_model_callback around every model call, and you attach them without touching the tool or the agent. When you read a callback that logs a tool’s arguments and timing, you are reading timed with a different name. And when you write a tool that opens a connection, wrap it in a with block so a failed call never leaks the resource.