Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Closures & Decorators  ›  Lesson

Decorators

Decorators 24 minWrapping a function in before- and after-work without touching its body
You're building a piece ofQuarry — the middleware ring
This piece — timed_summary(): Wraps a stage so every call is timed, without touching the stage's code or losing its name.
Scenario A nightly Quarry run took four hours instead of one, and the only way to find the slow stage without editing forty of them is to wrap them all in the same timer.
Your task
Write the decorator timed(func): its wrapper accepts any arguments, times the call with time.perf_counter(), appends the elapsed seconds to wrapper.timings, returns the original's result, and keeps the stage's identity with @wraps. Then write timed_summary(rows), which wraps the provided parse_row, calls it once per row, and returns {"name", "calls", "results"}. Example: timed_summary(["a,b", "c"]) → {"name": "parse_row", "calls": 2, "results": [["a", "b"], ["c"]]}.

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.

A wrapper function standing in front of the original, doing before-work, calling the original through a cell, then after-work, with @wraps(func) copying __name__ and __doc__ back onto the wrapper

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:

python

@timed above def parse_row is exactly that reassignment, written once:

python

The decorator itself runs once, at definition time — not every call:

What does this print?
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:

broken — fix itTypeError

Run 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:

What does this print?
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:

step through it
1def timed(func):
2 def wrapper(*args, **kwargs):
3 before = f"start {func.__name__}"
4 result = func(*args, **kwargs)
5 after = f"end {func.__name__}"
6 return result
7 return wrapper
8 
9@timed
10def parse_row(row):
11 fields = row.split(",")
12 return fields
13 
14out = parse_row("id,ts,path")
15name_now = parse_row.__name__
16print(out, name_now)

Here is the same call as a picture. The wrapper's frame opens around the original and closes after it:

Drawing diagram…

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:

What does this print?
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:

python

Treat @wraps(func) as part of the pattern, not an optional extra.

One-liner: stacking runs bottom-up

python

@tag("outer") over @tag("inner") means value = outer(inner(value)) — the decorator nearest the def wraps first, so it runs innermost.

Quick practice

python

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.

What does this print?
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"))
broken — fix itTypeError

Run it and read which function the error points at. Make the wrapper accept whatever the original takes.

What does this print?
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. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upYour first wrapper

Write a decorator shouted(func) whose wrapper calls func and returns its result in upper case. Then write shout_greeting(name) that returns shouted(greet)(name), where greet(name) returns f"hello {name}".

decoratorsfirst-class-functions
DrillA wrapper that remembers its calls

Write logged(func): its wrapper appends a line like "square(3)" (the function's name, then the positional arguments' reprs joined by ", ") to a list stored as wrapper.calls, then returns the result. Write call_log(values), which wraps the given square, calls it on each value and returns wrapper.calls.

decoratorsclosures
BuildWhat @wraps actually copies

Write identity_after_wrap(use_wraps). It decorates the provided parse_row with a do-nothing wrapper — using @wraps(func) only when use_wraps is true — and returns [name, doc]: the decorated function's __name__ and __doc__.

decoratorswraps
BossWhich layer is outside?

Write two decorators: bracket wraps the result in square brackets, exclaim adds "!" to the end. Write stacked_both(word) returning a list of two results for echo(word) (which returns word): first with @bracket above @exclaim, then with @exclaim above @bracket.

decoratorsstacked-decorators
CapstoneCounting calls across stages

Write counted(func): its wrapper increments wrapper.count on each call and returns the result, and it keeps the stage's name via @wraps. Write stage_counts(calls) where calls is a list of [stage_name, text] pairs naming "parse" or "clean". Wrap both provided stages fresh, run each call, and return {"outputs": [...], "counts": {name: count}}, with counts keyed by each wrapped stage's __name__.

decoratorswrapsclosures
ExplainWhat breaks without @wraps

A teammate says @wraps is cosmetic and removes it from Quarry's timing decorator to save a line. Explain what actually changes, and give two concrete ways it would hurt a program like Quarry.

timed_summary.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
timed_summary(rows) → dictWrap parse_row in the timer, run every row through it, and report its name, how many timings were recorded and the results.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.