Course outline · 0% complete

0/27 lessons0%

Course overview →

Decorators

lesson 5-2 · ~15 min · 14/27

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 after func, print the difference.
  • Caching: store (args) → result in a dict inside the closure, skip recomputation on repeat calls. The standard library ships this as functools.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.