Decorators: wrappers you can see

@timer is syntax for a function that calls another function. Class decorators wrap a whole type. If you cannot write wrap(fn), you are not ready to decorate.

PASSFAIL

PASS / FAIL card

Expected vs actual is a test.

You will be able to

  • Wrap a call with start/end logs without changing the inner spec
  • Keep the inner function testable without the wrapper
  • See @timer as syntax for wrap(fn)

Before the slow report, say start and end

That wrapper is the idea behind decorators. Here it is wrap calling percent — visible, testable, no mystery. On a laptop, @timer above def report(): is syntax that builds the same wrapping. If you cannot write wrap(fn), you are not ready to decorate.

The inner function stays pure: percent returns a number. The wrapper logs. Tests call percent directly. Ops look at INFO lines. Mixing them is how a unit test needs a log file.

Wrappers you actually want

Timing a slow attendance CSV. Checking that the caller is allowed to run a job. Retrying a weather GET a bounded number of times. Each is a wrapper. None of them should change the meaning of the inner result — only observe or decide whether to call.

functools.wraps keeps the inner name in traces. Class decorators wrap a whole type — same idea, bigger surface. Copy the sample; do not paste @ into this Run box.

Words that matter

Wrapper
A function that calls another, adding logging, timing or checks.
Decorator
Laptop syntax (@name) that applies a wrapper.
Inner function
The job itself — still testable alone.

Common mistakes

Avoid: A decorator that changes the return value ‘a bit’ with no test.

Do this: Wrapper observes; inner result stays the spec.

Run it step by step

Each box is a real program. Press Run, change a number, Run again — the output must follow your code.

1. Step 1 — inner stays simple

Tests would call percent directly.

Python sandboxlesson://workspace
console

Edit the example, press Run, then Build if you want a compile check.

build

Press Build to compile.

2. Step 2 — wrap() calling percent()

Start/end are extra. 90 is still the spec.

Python sandboxlesson://workspace
console

Edit the example, press Run, then Build if you want a compile check.

build

Press Build to compile.

On a full Python install — functools.wraps + a timer

Keep the inner name in traces. Time utilisation without editing percent’s body.

No pip — stdlib.

Decorator form of the same wrap() you will run in the sandbox.

Real library code (not run in this browser sandbox)

from functools import wraps
from time import perf_counter

def timed(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        t0 = perf_counter()
        out = fn(*args, **kwargs)
        print("INFO ms", round((perf_counter() - t0) * 1000, 1))
        return out
    return wrapper

@timed
def percent(used, seats):
    return used * 100 / seats

Example program — Check, then call

A guard wrapper. Inner percent never sees seats 0.

Python sandboxlesson://workspace
console

Edit the example, press Run, then Build if you want a compile check.

build

Press Build to compile.

Your turn — Wrap with INFO

Call a function that returns 90. Print INFO and 90.

Python sandboxlesson://workspace
console

Edit the example, press Run, then Build if you want a compile check.

build

Press Build to compile.

Self-assessment

Check your understanding before you mark the lesson complete.

1. A decorator is…
2. Unit tests should usually call…

Progress is stored in a browser cookie on this device.