W3docs

Python Decorators

Learn how Python decorators work: writing your own, preserving metadata with functools.wraps, stacking decorators, and real-world use cases.

A decorator is a function that wraps another function to extend or modify its behavior without changing its source code. Decorators are one of Python's most powerful and idiomatic features — they are the engine behind @staticmethod, @classmethod, @property, @functools.lru_cache, and many popular web framework patterns.

This page covers how decorators work, how to write your own from scratch, how to pass arguments to decorators, how to stack them, and when each pattern is most useful.

How Decorators Work

A decorator is just a function that takes another function as its argument and returns a new function. Python provides the @ syntax as a shorthand for applying one:

@shout
def greet(name):
    return f"hello, {name}"

That is exactly equivalent to:

def greet(name):
    return f"hello, {name}"

greet = shout(greet)

The @shout line tells Python: after defining greet, immediately pass it to shout and rebind the name greet to whatever shout returns. From that point on, every call to greet(...) goes through shout's logic first.

Writing Your First Decorator

A decorator normally defines an inner wrapper function that calls the original function and adds extra behavior around it:

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("world"))   # HELLO, WORLD
print(greet("python"))  # HELLO, PYTHON

wrapper accepts *args and **kwargs so it forwards any combination of arguments to func unchanged. This makes the decorator compatible with any function regardless of its signature — a good habit from the start.

Why the Wrapper Must Return the Inner Function

shout ends with return wrapper, not return wrapper(). This is intentional: shout is building a new callable, not calling it yet. If you accidentally wrote return wrapper(), the decorator would run immediately at decoration time and greet would be bound to the return value of wrapper — a string — rather than to the callable itself.

Preserving Metadata with functools.wraps

Every Python function carries metadata: __name__, __doc__, __module__, and more. Without extra care, a decorator replaces the original function with wrapper, losing all of that:

def shout(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet.__name__)  # wrapper  — wrong
print(greet.__doc__)   # None     — lost

Fix this by applying @functools.wraps(func) to the wrapper. It copies the original function's metadata onto wrapper:

import functools

def shout(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    """Say hello to name."""
    return f"hello, {name}"

print(greet("world"))  # HELLO, WORLD
print(greet.__name__)  # greet
print(greet.__doc__)   # Say hello to name.

Always use @functools.wraps in any decorator you write. Without it, debugging tools, documentation generators, and test frameworks see the wrong function name. The only exception is when you intentionally want to hide the original identity.

Practical Decorator Examples

Logger

Log every call to a function with its arguments and return value:

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

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

add(3, 5)
# Calling add with args=(3, 5) kwargs={}
# add returned 8

Timer

Measure how long a function takes to run:

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.6f}s")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

total = slow_sum(1_000_000)
print(total)  # slow_sum took 0.01xxs  then  499999500000

time.perf_counter() is the right choice here because it has the highest available resolution for short-duration measurements.

Memoization (Cache)

Cache the return value for each unique set of arguments so the function is never computed twice for the same input:

import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 55
print(fibonacci(30))  # 832040

For production code, prefer the built-in @functools.lru_cache or @functools.cache (Python 3.9+), which handle edge cases, thread safety, and cache size limits. The hand-rolled version above is useful to understand the pattern.

Access Control

Guard a function so it can only run when a condition is met:

import functools

def require_auth(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated"):
            raise PermissionError("Authentication required.")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def get_dashboard(user):
    return f"Welcome, {user['name']}!"

guest = {"name": "Guest", "is_authenticated": False}
admin = {"name": "Admin", "is_authenticated": True}

try:
    print(get_dashboard(guest))
except PermissionError as e:
    print(e)                      # Authentication required.

print(get_dashboard(admin))       # Welcome, Admin!

Decorators with Arguments

Sometimes you need to configure a decorator at decoration time — for example, to repeat a function a variable number of times. Plain decorators cannot take extra arguments directly because Python passes the function, not the arguments. The solution is a decorator factory: a function that accepts the configuration and returns a decorator:

import functools

def repeat(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say(message):
    print(message)

say("hello")
# hello
# hello
# hello

Reading from the outside in: @repeat(3) first calls repeat(3), which returns decorator. Python then applies decorator to say, which returns wrapper. So say ends up pointing at wrapper — same pattern as before, with the extra level just to carry n into scope.

The nesting can look daunting at first. A mental shorthand: the outermost function holds the configuration, the middle function holds the function being decorated, and the innermost function holds the call being intercepted.

Stacking Multiple Decorators

You can apply several decorators to a single function by stacking @ lines. Python applies them bottom-up — the decorator closest to the def is applied first:

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<b>" + func(*args, **kwargs) + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<i>" + func(*args, **kwargs) + "</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("Alice"))  # <b><i>Hello, Alice</i></b>

Equivalent to greet = bold(italic(greet)). italic wraps greet first, then bold wraps the result. The output shows italic runs closer to the raw string and bold wraps the outside.

Class-Based Decorators

A class can be a decorator too — any object with a __call__ method is callable. Class-based decorators are useful when the decorator itself needs to maintain state across calls:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()
print(say_hello.count)  # 2

functools.update_wrapper(self, func) does the same job as @functools.wraps — it copies the original function's metadata onto the instance. After decoration, say_hello is a CountCalls instance, so say_hello.count is a regular attribute access.

When to choose a class over a function decorator:

  • You need persistent state (count, cache, flags).
  • The decorator has multiple methods or helper logic.
  • You need the decorated object to be introspectable as a specific type.

Decorator Gotchas

Forgetting to call the decorated function

A common early mistake is returning the wrapper but forgetting to call func inside it:

def broken(func):
    def wrapper(*args, **kwargs):
        print("before")
        # forgot to call func!
    return wrapper

The decorated function silently returns None every time. Always make sure wrapper calls func(*args, **kwargs) and returns its result.

Decorating at the wrong layer

When using parameterized decorators, forgetting the outer call is a frequent mistake:

# Wrong — 'repeat' receives the function, not a count
@repeat      # should be @repeat(3)
def say(msg):
    print(msg)

This passes say to repeat where n is expected, causing a TypeError when say is called.

Decorator order matters

With stacked decorators the order changes behavior. @timer then @log_calls on the same function will time the already-logged version, while the reverse will log the already-timed version. Think through what you want each layer to see.

Relationship to Closures

A decorator's wrapper function is a closure — it captures func from the enclosing scope and keeps it alive even after the outer decorator function has returned. Understanding closures makes decorator internals obvious: the cell object holding func is exactly what lets wrapper call the original function long after shout(greet) completed.

For *args and **kwargs syntax used inside wrappers, see the dedicated chapter. For lambda expressions that pair well with decorators in higher-order patterns, see the lambda chapter.

Quick Reference

PatternWhen to use it
Basic wrapperAdd behavior before/after a function
@functools.wrapsAlways — preserves __name__, __doc__
Decorator factory (3 levels)Need to configure the decorator
Stacked decoratorsCompose multiple independent behaviors
Class-based decoratorNeed persistent state between calls
@functools.lru_cacheMemoize pure functions (built-in, production-ready)

Practice

Practice
What does @functools.wraps(func) do inside a decorator?
What does @functools.wraps(func) do inside a decorator?
Practice
Given @bold applied above @italic on a function, which decorator is applied first?
Given @bold applied above @italic on a function, which decorator is applied first?
Practice
What is a decorator factory?
What is a decorator factory?
Was this page helpful?