C.W.K.
Stream
Lesson 01 of 05 · published

Functions Wrapping Functions — The Decorator Idea

~22 min · decorator, wrapper, first-class, @

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

What @ actually does

A decorator is a function that takes a function and returns a function. The @decorator syntax above a definition is just shorthand: @deco + def f(): ... is exactly the same as def f(): ... followed by f = deco(f). That's it. Once you see this, every decorator stops being magic.

The wrapping pattern — the canonical shape

You define a function (the decorator) that takes another function as input. Inside, you define an inner wrapper function that calls the original — possibly with extra logic before, after, or around the call. You return the wrapper. When the user writes @deco, they get the wrapper bound to the original name.

Why we need *args and **kwargs in the wrapper

The decorator doesn't know what arguments the wrapped function takes. If the wrapper hard-codes def wrapper():, it only works for zero-arg functions. The idiomatic shape is def wrapper(*args, **kwargs): return fn(*args, **kwargs). The wrapper accepts everything and forwards everything. You add your extra logic before or after the call.

What decorators are good for

Cross-cutting concerns. Logging that happens around every API call. Timing measurements. Caching of expensive computations. Authentication checks. Retry logic. Anything that conceptually wraps the function rather than living inside its body. The pattern is, "I want to add behavior at the boundary of every call without modifying the function itself."

Principle: Decorators are composition. A function is a value, and a decorator transforms one value into another. There's no special "decorator mechanism" in the language — just a function that takes a function and a sugar syntax. Everything else builds on this single idea.

Stacking — multiple decorators on one function

Decorators stack from bottom up. @a + @b + def f(): means f = a(b(f)). The closest decorator to the function is applied first. This matters when ordering matters (logging-then-cache vs. cache-then-logging give different results).

Code

@deco is just sugar for f = deco(f)·python
def announce(fn):
    def wrapper():
        print("calling", fn.__name__)
        result = fn()
        print("finished")
        return result
    return wrapper

# These two are EXACTLY equivalent:

# Form 1 — manual
def hello():
    print("hi")
hello = announce(hello)

# Form 2 — sugar
@announce
def hello():
    print("hi")

hello()
# calling hello
# hi
# finished
The forward-everything wrapper pattern·python
import time

def timed(fn):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} took {elapsed*1000:.2f}ms")
        return result
    return wrapper

@timed
def slow_add(a, b):
    time.sleep(0.1)
    return a + b

print(slow_add(3, 4))
# slow_add took ~100ms
# 7

# Works with any signature because of *args/**kwargs forwarding
@timed
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

print(greet("Pippa", greeting="Hi"))
Logging — the most common real use·python
def log_calls(fn):
    def wrapper(*args, **kwargs):
        arglist = ", ".join(
            [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
        )
        print(f"call: {fn.__name__}({arglist})")
        result = fn(*args, **kwargs)
        print(f"  -> {result!r}")
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

add(3, 4)
# call: add(3, 4)
#   -> 7

add(b=10, a=2)
# call: add(a=2, b=10)
#   -> 12
Stacking decorators — bottom-up·python
def shout(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper

def exclaim(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs) + "!"
    return wrapper

# @shout is applied LAST — closest to call site
# @exclaim is applied FIRST — closest to function
@shout
@exclaim
def greet(name):
    return f"hello {name}"

print(greet("pippa"))   # 'HELLO PIPPA!'

# Equivalent to: greet = shout(exclaim(greet))
# 1. exclaim wraps greet to add '!'
# 2. shout wraps THAT to uppercase

External links

Exercise

Write a decorator count_calls(fn) that, in addition to calling fn and returning its value, keeps a count of how many times the wrapped function has been called. The count should be accessible as wrapped_fn.calls. Test by decorating two different functions and confirming each has its own independent count. (Hint: store the count on the wrapper itself — wrapper.calls = 0 then increment.)

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.