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.

10 min Free

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.

Ready
# 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 ← →.

00/12

Variables

Nothing defined yet.

Output

Nothing printed yet.

What to notice

  1. Decoration happens at definition time. @timed runs when Python reads the def, long before anyone calls get_pincode. After it, the name get_pincode points at wrapper.
  2. *args, **kwargs pass anything through. The wrapper does not know or care what arguments the original takes. It forwards them all and returns whatever came back.
  3. functools.wraps keeps 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.
  4. yield splits setup from cleanup. In open_ticket, everything before yield runs on entering the with, the yielded value becomes ticket, and everything after yield runs on exit.
  5. finally makes cleanup unconditional. If the block raised, the exception would be re-raised at the yield inside the generator, and finally would still print close. That guarantee is the reason to use with instead of calling close() yourself.
  6. 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.

Check your understanding

1. What is @timed above def get_pincode(...) short for?
2. You drop @functools.wraps(func) from the decorator. What changes?
3. Code inside a with open_ticket('T-1') as t: block raises an exception. Does 'close T-1' still print?