Focused practice · 9 min read

Python Decorators Practice Guide: Wrappers

Decorators are a small idea with a large payoff: a function can be passed to another function and replaced by the result. Once that clicks, logging, timing, caching, and validation become reusable layers instead of copy-pasted code.

Functions are ordinary objects

You can assign a function to a name, pass it as an argument, and return it. A decorator is simply a function that uses those abilities. It receives the original function and returns a callable that usually calls it.

def greet():
    return "hello"

def call(func):
    return func()

print(call(greet))

Passing a function

Build a wrapper

The inner function closes over the original function. The outer decorator returns that inner function. Nothing runs inside the wrapper until the decorated function is called.

def announce(func):
    def wrapper():
        return "Before: " + func()
    return wrapper

@announce
def greet():
    return "hello"

print(greet())

The wrapper runs later

Read the @ shorthand

The @ line is applied after the function definition. @announce above greet means greet = announce(greet). The name greet now points to the wrapper, which is why the wrapper must call the original function explicitly.

def upper(func):
    def wrapper():
        return func().upper()
    return wrapper

@upper
def word():
    return "hi"

print(word())

A name replaced by a wrapper

Preserve metadata with wraps

Without help, the wrapper's __name__ is wrapper. functools.wraps copies the original name, docstring, and related attributes. This matters for debugging, documentation, and tools that inspect functions.

from functools import wraps

def upper(func):
    @wraps(func)
    def wrapper():
        return func().upper()
    return wrapper

@upper
def word():
    return "hi"

print(word.__name__)

Keep the original identity

Forward arguments

A wrapper with no parameters only works for functions with no parameters. Use *args and **kwargs to accept whatever the original function accepts, then forward the values unchanged. Keep the wrapper's own logic outside the original call.

def announce(func):
    def wrapper(*args, **kwargs):
        return "Result: " + str(func(*args, **kwargs))
    return wrapper

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

print(add(2, 3))

A general wrapper

Your practice checklist

Common questions

Can a decorator take arguments?

Yes. You then need another layer: a function that accepts the arguments and returns the actual decorator.

Does @ change the function immediately?

It replaces the name at definition time. The wrapper body runs later when the new name is called.

Are decorators only for functions?

Classes can be decorated too, but start with functions. The same idea applies whenever a callable can be replaced by another callable.

Practice decorators

12 questions on wrappers, @ syntax, functools.wraps, and forwarding arguments.

Open practice

Keep reading: