Wrapping a function with extra behavior
You cannot read a modern Python codebase without decorators: web routes, test fixtures, caching, and permission checks are all spelled with an @ line. The good news is that a decorator is nothing but the closure pattern from lesson 5-1 plus a little syntax.
Suppose you want several functions to announce when they run. Editing each one is repetitive. Instead, write a function that takes a function and returns a wrapped version:
def announce(func): def wrapper(*args, **kwargs): print(f"calling {func.__name__}") return func(*args, **kwargs) return wrapper
This is just lesson 5-1's closure pattern. wrapper remembers func, and the *args, **kwargs from lesson 2-1 let it forward any arguments. A function that transforms another function like this is a decorator, and Python gives it dedicated syntax:
@announce def greet(name): return f"hi {name}"
@announce means exactly greet = announce(greet). Nothing more.
Code exercise · python
Run this program. Both decorated functions get the announcement for free, with their arguments and return values passing straight through.
Practical decorators you will actually meet
- Timing: record
time.perf_counter()before and afterfunc, print the difference. - Caching: store
(args) → resultin a dict inside the closure, skip recomputation on repeat calls. The standard library ships this asfunctools.lru_cache, coming in lesson 6-3. - Access control and logging: web frameworks like Flask use decorators such as
@app.route("/")to register functions.
One piece of hygiene: wrapping replaces the function's name and docstring (the optional help text in triple quotes right under def, which help() displays) with the wrapper's. Put @functools.wraps(func) on the wrapper to copy the original's identity over. Do that in any decorator you write for real code.
Code exercise · python
Run this program. A decorator can even attach data to the wrapper it returns: count_calls stores a counter on the wrapper function itself, so ping.calls is readable from outside. Real profiling and rate-limiting decorators use this exact attach-state trick.
Code exercise · python
Your turn. Write a decorator retry_twice(func) that calls func, and if it raises ValueError, prints "retrying" and calls it once more. Decorate flaky, which fails on its first call only.
Quiz
What is @announce above def greet exactly equivalent to?
Problem
A decorator's wrapper is defined as def wrapper(*args, **kwargs). From lesson 2-1, what TYPE does kwargs have inside wrapper? One word.