PythonDecorators

Decorators

A decorator is a function that wraps another function to extend or modify its behavior, without changing the original function's code. Decorators build directly on closures — a decorator is a closure that remembers the function it wraps.

The `@decorator` Syntax

The @decorator line placed above a function definition is just syntactic sugar. It is exactly equivalent to reassigning the function name to the result of calling the decorator on it.

Python
def shout(func):
    def wrapper():
        return func().upper() + "!"
    return wrapper


@shout
def greet():
    return "hello"


print(greet())
HELLO!

The @shout line above is identical to writing this manually:

Python
def greet():
    return "hello"


greet = shout(greet)  # same effect as @shout
print(greet())
HELLO!
Writing a Timing Decorator From Scratch

A very common real-world decorator measures how long a function takes to run. Since the wrapped function might take arguments, the inner wrapper needs *args/**kwargs to forward whatever it receives.

Python
import time


def timer(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.021374s
499999500000

wrapper runs the timing logic, calls the real function through func(*args, **kwargs), and returns its result — the caller of slow_sum never notices anything changed except the extra printed timing line.

Decorators With Arguments (Decorator Factories)

Sometimes you want to configure the decorator itself, e.g. @retry(times=3). This requires an extra layer of nesting: an outer function that takes the decorator's own arguments and returns the actual decorator.

Python
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator


@repeat(times=3)
def say_hi():
    print("Hi!")


say_hi()
Hi!
Hi!
Hi!

@repeat(times=3) first calls repeat(times=3), which returns decorator. That decorator is then applied to say_hi, exactly like a normal one-argument decorator. Three nested functions — repeat, decorator, wrapper — is the standard shape for any decorator that itself takes arguments.

Preserving Metadata with `functools.wraps`

There's a subtle problem with the decorators above: the wrapped function loses its original name and docstring, because wrapper — not the original function — is what actually gets bound to the decorated name.

Python
def timer(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@timer
def slow_sum(n):
    """Return the sum of numbers from 0 to n-1."""
    return sum(range(n))


print(slow_sum.__name__)  # 'wrapper', not 'slow_sum'!
print(slow_sum.__doc__)   # None, docstring is lost!
wrapper
None
Warning
Without `functools.wraps`, tools like `help()`, debuggers, and introspection-based libraries see the decorated function as a generic `wrapper` with no docstring — which makes debugging and documentation misleading. Always fix this with `@functools.wraps`.

Python
import functools


def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@timer
def slow_sum(n):
    """Return the sum of numbers from 0 to n-1."""
    return sum(range(n))


print(slow_sum.__name__)  # 'slow_sum' — correct!
print(slow_sum.__doc__)   # original docstring — correct!
slow_sum
Return the sum of numbers from 0 to n-1.
Tip
`functools.wraps(func)` is itself a decorator (a decorator factory, in fact) that copies `__name__`, `__doc__`, and other metadata from the original function onto `wrapper`. Get in the habit of adding it to every decorator you write — it costs one line and one import.
Built-in Decorators: A Preview

Python ships several decorators used constantly in object-oriented code. They're covered in full in the classes and OOP section, but it's worth knowing they exist:

Decorator

Purpose

@staticmethod

Defines a method that doesn't receive self or cls — behaves like a plain function namespaced inside a class

@classmethod

Defines a method that receives the class (cls) instead of an instance — often used for alternate constructors

@property

Lets a method be accessed like an attribute, without parentheses — useful for computed or validated attributes

Quick Reference

Concept

Syntax

Basic decorator

@decorator above def func():

Equivalent without sugar

func = decorator(func)

Decorator factory

@decorator(arg) — an extra outer function layer

Preserve metadata

@functools.wraps(func) on the inner wrapper

Example
A decorator that only allows a function to run if a condition is met, otherwise raises an error — a simplified authorization check.

Python
import functools


def require_login(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_logged_in"):
            raise PermissionError(f"{user['name']} must log in first.")
        return func(user, *args, **kwargs)
    return wrapper


@require_login
def view_dashboard(user):
    print(f"Welcome to your dashboard, {user['name']}!")


view_dashboard({"name": "Ada", "is_logged_in": True})
view_dashboard({"name": "Grace", "is_logged_in": False})
Welcome to your dashboard, Ada!
PermissionError: Grace must log in first.
Note
Multiple decorators can be stacked on one function — they apply bottom-up, closest to the function first. `@a` `@b` `def f():` is equivalent to `f = a(b(f))`.