Decorators
In short: a decorator is a function that takes a function and returns an
improved one — @dec written above a def means exactly name = dec(name),
run once, right when the def executes, not on every call. The wrapper it
returns should accept *args, **kwargs and forward them, so it fits any
function's signature, and it must return what the original returned, or every
caller silently gets None back. Without @functools.wraps(func) on the
wrapper, the decorated function's __name__ and __doc__ become the wrapper's
— which breaks logs, docs and anything that introspects it — so every wrapper
you write starts with @wraps(func). Decorators stack bottom-up: the one
nearest the def wraps first and ends up innermost.

Time a stage without touching it
Pasting timing code into every stage drifts and clutters. A decorator writes it once and wraps it around any function instead:
@timed above def parse_row is exactly that reassignment, written once:
The decorator itself runs once, at definition time — not every call:
events = []
def noted(func):
events.append("decorate")
return func
@noted
def parse(row):
events.append("call")
return row
events.append("defined")
parse("x")
print(events)Wrap a stage that takes extra arguments
A wrapper written for one argument breaks on anything else:
TypeErrorRun it and read which argument the wrapper choked on. Make the wrapper accept, and forward, any arguments at all.
*args/**kwargs collect anything the caller passed and spread it back out
unchanged — the shape you'll write almost every time. The other classic slip is
forgetting the wrapper has to return the original's result:
def announce(func):
def wrapper(*args, **kwargs):
print("calling", func.__name__, end=" -> ")
func(*args, **kwargs) # called, but the result is thrown away
return wrapper
@announce
def double(n):
return n * 2
print(double(4))Watching one call go through the wrapper
Step through a single call. Watch func and args appear inside wrapper's
frame, the original run, and result come back through the wrapper before the
caller sees it:
Here is the same call as a picture. The wrapper's frame opens around the original and closes after it:
Look at the last value in the trace, though. name_now is "wrapper".
Trace a bug logged as "wrapper"
The decorated name is bound to wrapper, so it looks like wrapper to
anything that asks:
def timed(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timed
def parse_row(row):
"""Split one CSV row into fields."""
return row.split(",")
print(parse_row.__name__, parse_row.__doc__)@wraps(func), from the standard library, copies the original's identity onto
the wrapper:
Treat @wraps(func) as part of the pattern, not an optional extra.
One-liner: stacking runs bottom-up
@tag("outer") over @tag("inner") means value = outer(inner(value)) — the
decorator nearest the def wraps first, so it runs innermost.
Quick practice
wrapper.timings = [] runs once, at decoration time, so every decorated
function gets its own list. Inside wrapper, the name wrapper is itself a
free variable — it refers to the function object, which is how a call finds its
own list to append to.
def shout(func):
def wrapper(*a, **kw):
return func(*a, **kw).upper()
return wrapper
def exclaim(func):
def wrapper(*a, **kw):
return func(*a, **kw) + "!"
return wrapper
@shout
@exclaim
def greet(name):
return f"hi {name}"
print(greet("sam"))TypeErrorRun it and read which function the error points at. Make the wrapper accept whatever the original takes.
calls = []
def counted(func):
def wrapper(*args, **kwargs):
calls.append(1)
return func(*args, **kwargs)
return wrapper
@counted
def ping():
return "pong"
ping(); ping(); ping()
print(len(calls))🎯 Your turn
Write the decorator timed(func). Its wrapper must accept any arguments, time
the call with time.perf_counter(), append the elapsed seconds to a list
stored as wrapper.timings, and return the original's result. Use
@wraps(func) so the stage keeps its name.
Then write timed_summary(rows): wrap the provided parse_row with timed,
call the wrapped version once per row, and return
{"name": ..., "calls": ..., "results": [...]} — the wrapped function's
__name__, the number of timings recorded, and each call's result.
timed_summary(["a,b", "c"])→{"name": "parse_row", "calls": 2, "results": [["a", "b"], ["c"]]}
Hint: wrap inside timed_summary (parse = timed(parse_row)) so each run
starts with a fresh, empty timings list.
Press ▶ Run, then ✓ Check. ✅
