Phase 4 · Intermediate PythonModule 24~36 min read

Decorators

Wrap and enhance functions cleanly — the pattern behind so much of Python.

What you'll learn

Decorators wrap a function to add behavior — logging, timing, caching, access checks — without touching the function's own code. You've already met a few (@dataclass, @lru_cache, @property). Now you'll understand exactly how they work and write your own.

By the end you'll be able to:

  • Explain what the @decorator syntax really does
  • Write a decorator that wraps any function
  • Preserve metadata with functools.wraps
  • Write decorators that take arguments

The core idea

A decorator is just a function that takes a function and returns a new function — closures and higher-order functions from the last module, put to work. The new function usually calls the original with some extra behavior around it:

idea.py
def loud(func):                  # takes a function
    def wrapper():
        print("BEFORE")
        func()
        print("AFTER")
        return wrapper           # returns a new function
    return wrapper

def greet():
    print("Hello")

greet = loud(greet)              # wrap it manually
greet()

A simple decorator

The @decorator line above a function is just syntactic sugar for greet = loud(greet). To wrap any function, the wrapper accepts *args, **kwargs and passes them through — and returns the original's result:

decorator.py
def loud(func):
    def wrapper(*args, **kwargs):     # accept ANY arguments
        print("BEFORE")
        result = func(*args, **kwargs)
        print("AFTER")
        return result
    return wrapper

@loud                                 # syntactic sugar for greet = loud(greet)
def greet(name):
    print(f"Hello, {name}")
    return name

greet("Ada")

Key idea

@loud above greet does exactly one thing: replaces greet with loud(greet). Every decorator, no matter how fancy, is this substitution. Once you see the sugar, decorators stop being mysterious.

Preserving metadata

There's a catch: after wrapping, greet.__name__ becomes "wrapper" and its docstring is lost — confusing for debugging and tools. Apply @functools.wraps(func) to the wrapper to copy the original's identity across:

wraps.py
from functools import wraps

def loud(func):
    @wraps(func)                      # copy name, docstring, etc. from func
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@loud
def greet(name):
    "Greets a person."
    return f"Hi {name}"

print(greet.__name__)     # greet  (without @wraps it would say 'wrapper')
print(greet.__doc__)      # Greets a person.

Tip

Always add @wraps(func) to your wrappers. It's a one-line habit that keeps names, docstrings, and signatures intact — so introspection, help text, and debuggers still work on the decorated function.

Decorators with arguments

To pass arguments to a decorator (like @repeat(times=3)), you need one more layer: a decorator factory — a function that takes the arguments and returns a decorator. Three nested functions, but the pattern is mechanical:

factory.py
from functools import wraps

def repeat(times):                    # a decorator FACTORY
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()

Real-world uses

Decorators shine for cross-cutting concerns — behavior many functions share:

  • Timing / logging — measure or record every call.
  • Caching — @lru_cache memoizes results.
  • Access control — @login_required in web frameworks.
  • Registration & routing — @app.route("/") in Flask.
  • Retries — re-run a flaky call a few times.

Recap & quick check

Key takeaways

  • A decorator is a function that takes a function and returns a new (wrapping) function.
  • @decorator above a function is sugar for func = decorator(func).
  • Make wrappers accept *args, **kwargs and return the result so they work on any function.
  • Add @functools.wraps(func) to preserve the original's name, docstring, and signature.
  • Decorators that take arguments need an extra layer (a decorator factory); great for logging, caching, auth, routing.

Quick check

1. What does @loud above a function mean?

2. Why do wrappers use *args, **kwargs?

3. What does @functools.wraps(func) do?

4. How do you make a decorator that takes arguments, like @repeat(3)?

5. Which is a typical use of decorators?

One more resource-management pattern completes Phase 4's toolkit. Next up: Module 25 — Context Managers.