Filter concepts by levelShowing all levels.

Python · Section 5

Decorators

Level
intermediate
Read
95 min
Concepts
8

A decorator wraps a function or class with extra behaviour without changing its source — the single mechanism behind logging, caching, validation, registration and dozens of other cross-cutting concerns in real Python code.

What is true here

  1. @my_decorator above a def is exactly name = my_decorator(name), run once at def time.
  2. Stacked decorators apply bottom-up (closest to def first) but run outer-to-inner at call time.
  3. A decorator with arguments needs three nested functions, not two — the extra layer takes its own config.
  4. @functools.wraps(func) on wrapper is what keeps __name__, __doc__ and __wrapped__ pointed at the original.
  5. A class implementing __call__ can decorate a function too, with state as plain, readable attributes.

What you will be able to do

  • Write a function decorator whose wrapper correctly forwards every argument and the return value
  • Write a decorator that takes its own configuration arguments, using the three-layer nested pattern
  • Predict the exact call order of several stacked decorators, and explain why it is not the write order
  • Apply functools.wraps and explain exactly what breaks without it
  • Decorate a method or a class, and explain what each receives and must return
  • Write a class-based decorator using __call__, and say when it beats a closure-based one

Writing a decorator

The core mechanism — a function that replaces a function — and the two ways to give it its own configuration.

Function decorators

coreintermediate

A decorator is a function that takes a function and returns a replacement for it. @my_decorator above def process(): is exactly process = my_decorator(process), just written before the definition instead of after.

Think of it as

A decorator is a gift wrapper placed around a function. The original function still exists and still runs, but every call now goes through the wrapping first — the wrapper decides whether to add something before, after, or around the call, without the caller ever needing to know the wrapping is there.

python
def my_decorator(func):
    def wrapper(*args, **kwargs):
        # do something before
        result = func(*args, **kwargs)
        # do something after
        return result
    return wrapper

@my_decorator
def process():
    pass

What we're doing: Show that @my_decorator above process() is exactly equivalent to reassigning process = my_decorator(process) by hand.

decorator_equivalence.pypython
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function runs")
        result = func(*args, **kwargs)
        print("After the function runs")
        return result
    return wrapper


@my_decorator
def process():
    print("Processing...")


process()

print("--- manual equivalent ---")


def process2():
    print("Processing...")

process2 = my_decorator(process2)   # exactly what @my_decorator does
process2()
1
my_decorator takes one function and returns wrapper — a new function that will stand in for it.
9
@my_decorator runs my_decorator(process) immediately and rebinds the name process to whatever it returns.
22
process2 = my_decorator(process2) does by hand exactly what the @ syntax did automatically two lines above.
Output
Before the function runs
Processing...
After the function runs
--- manual equivalent ---
Before the function runs
Processing...
After the function runs

Why this works: process() and process2() print identical output because @my_decorator and the manual process2 = my_decorator(process2) line do the exact same thing — call my_decorator with the original function and rebind the name to its return value. The @ syntax is purely a shorter way to write that one reassignment, never a different mechanism.

wrapper forgets to return the original function's result

Wrong

python
def logs_but_eats_result(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        func(*args, **kwargs)   # called, but the result is dropped
    return wrapper

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

result = add(2, 3)
print(result)   # None — the real return value never escaped wrapper

Better

python
def logs_and_returns(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)   # forward the result
    return wrapper

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

result = add(2, 3)
print(result)

What you see: No exception — add(2, 3) silently prints None instead of 5, because wrapper never returns anything, so it defaults to None like any function that falls off the end.

Why: Once a decorator replaces process with wrapper, wrapper's return value IS the decorated function's return value from the caller's point of view — there is no other path back. Calling func(*args, **kwargs) without returning it discards the real result and silently substitutes None.

@my_decorator above def is sugar for one reassignment

def process(): ...

the original function object is built

my_decorator(process)

runs once, at def time, and returns wrapper

process = wrapper

the name process now points at wrapper instead

  1. def process(): ... — the original function object is built
  2. my_decorator(process) — runs once, at def time, and returns wrapper
  3. process = wrapper — the name process now points at wrapper instead

@decorator syntax and its plain-assignment equivalent

@decorator syntax and its plain-assignment equivalent
Written asRuns as
@my_decorator\ndef process(): ...def process(): ...\nprocess = my_decorator(process)
Decoration timingonce, when Python reads the def — not per call
What my_decorator receivesthe original process function object itself
What process becomeswhatever my_decorator returns — usually a wrapper

Together

python
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function runs")
        result = func(*args, **kwargs)
        print("After the function runs")
        return result
    return wrapper

@my_decorator
def process():
    print("Processing...")

process()

Remember: @my_decorator above def is exactly name = my_decorator(name) — decoration runs once at def time, and wrapper must forward every argument and the return value.

See also: decorators with arguments · functools wraps · stacked decorators · closures

Decorators with arguments

coreintermediate

@repeat(times=3) needs three nested functions, not two: the outer one takes the decorator's own arguments (times), the middle one is the real decorator that takes func, and the inner one is wrapper, exactly like a plain decorator.

Think of it as

A plain decorator is a machine that takes a function and returns a wrapper. A decorator with arguments is a machine that BUILDS that machine — call repeat(times=3) first, and what comes back is an ordinary decorator, already configured with times=3 baked in, ready to be applied to any function.

python
def repeat(times):          # 1. takes the decorator's own arguments
    def decorator(func):    # 2. takes the function being decorated
        def wrapper(*args, **kwargs):   # 3. runs on every call
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    return f"Hi, {name}"

What we're doing: Confirm that @repeat(times=3) actually applies its own argument, by comparing it against @repeat(times=1) on the same function.

repeat_decorator.pypython
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(times):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator


@repeat(times=3)
def greet(name):
    return f"Hi, {name}"


@repeat(times=1)
def greet_once(name):
    return f"Hi, {name}"


print(greet("Sam"))
print(greet_once("Sam"))
1
repeat(times) is the outer function — it takes the DECORATOR's own argument, not the function being decorated.
2
decorator(func) is the real decorator — this is what actually receives greet.
5
times is a closure variable here, captured from the repeat(times=3) call that built this particular decorator.
11
@repeat(times=3) calls repeat(times=3) first, then applies its return value (decorator) to greet.
Output
['Hi, Sam', 'Hi, Sam', 'Hi, Sam']
['Hi, Sam']

Why this works: greet and greet_once run the identical wrapper code, but produce different-length lists because each was built by a separate call to repeat() — repeat(times=3) closes over times=3, repeat(times=1) closes over a completely independent times=1. The argument passed to the decorator genuinely changes the behaviour of the wrapper it produces.

Missing the middle layer — writing a two-level decorator with arguments

Wrong

python
def repeat(times):
    def wrapper(*args, **kwargs):   # missing the decorator(func) layer
        return [None] * times
    return wrapper

@repeat(times=3)
def greet(name):
    return f"Hi, {name}"

greet("Sam")   # TypeError

Better

python
def repeat(times):
    def decorator(func):            # the missing layer, restored
        def wrapper(*args, **kwargs):
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    return f"Hi, {name}"

greet("Sam")

What you see: TypeError: 'list' object is not callable — because @repeat(times=3) applies repeat(times=3)'s return value (a list, once wrapper is called with no func) directly to greet as if it were a decorator.

Why: @repeat(times=3) always calls repeat(times=3) FIRST, then decorates with whatever comes back — that return value must itself be a decorator (a function that takes func and returns a wrapper). Skipping the middle layer means repeat immediately produces something that behaves like the final wrapper instead of a decorator, and Python tries to call ITS result as the decorator instead.

Three layers, called in order

repeat(times=3)

called first, returns decorator

decorator(greet)

called next, returns wrapper

greet = wrapper

times=3 stays captured inside it

  1. repeat(times=3) — called first, returns decorator
  2. decorator(greet) — called next, returns wrapper
  3. greet = wrapper — times=3 stays captured inside it

Plain decorator vs. a decorator that takes arguments

Plain decorator vs. a decorator that takes arguments
FormNestingCall at decoration time
@my_decoratordecorator(func) → wrapper(...)my_decorator(process)
@repeat(times=3)outer(times) → decorator(func) → wrapper(...)repeat(times=3) first, then decorator(process)

Together

python
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return [func(*args, **kwargs) for _ in range(times)]
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    return f"Hi, {name}"

print(greet("Sam"))

Remember: @repeat(times=3) calls repeat(times=3) first — its result must be an ordinary decorator, so a decorator with arguments needs one extra layer of nesting.

See also: function decorators · decorator factories · functools wraps

Decorator factories

coreintermediate

A decorator factory is a plain function that returns a decorator instead of being one — repeat(times=3) is a call, not a decorator, but its return value is. One factory, called differently, produces many independently configured decorators.

Think of it as

A decorator is one specific tool. A decorator factory is a tool-making machine — call it with different settings and it hands back a different, ready-to-use tool each time, without changing the machine itself. validate_range(0, 100) and validate_range(0, 5) come out of the same factory but enforce completely different rules.

python
def factory(config):
    def decorator(func):
        def wrapper(*args, **kwargs):
            # use config here
            return func(*args, **kwargs)
        return wrapper
    return decorator

configured_decorator = factory(some_config)   # call the factory first
@configured_decorator
def target():
    ...

What we're doing: Use one decorator factory to produce two independently configured decorators, and confirm each enforces its own range without interfering with the other.

validate_range_factory.pypython
import functools

def validate_range(minimum, maximum):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(value):
            if not (minimum <= value <= maximum):
                raise ValueError(f"{value} not in range [{minimum}, {maximum}]")
            return func(value)
        return wrapper
    return decorator


@validate_range(0, 100)
def set_volume(level):
    return f"volume set to {level}"


@validate_range(0, 5)
def set_priority(level):
    return f"priority set to {level}"


print(set_volume(80))
print(set_priority(3))
try:
    set_priority(9)
except ValueError as e:
    print("ValueError:", e)
try:
    set_volume(150)
except ValueError as e:
    print("ValueError:", e)
3
validate_range is the factory — an ordinary function, not a decorator itself, that BUILDS a decorator each time it is called.
15
validate_range(0, 100) is called first, producing an independent decorator closing over minimum=0, maximum=100, applied to set_volume.
20
validate_range(0, 5) is a SEPARATE call, producing a completely independent decorator with its own minimum/maximum, applied to set_priority.
Output
volume set to 80
priority set to 3
ValueError: 9 not in range [0, 5]
ValueError: 150 not in range [0, 100]

Why this works: set_priority(9) raises against range [0, 5] while set_volume(150) raises against range [0, 100] — proof that the two calls to validate_range() each built their own decorator with its own captured minimum and maximum, rather than sharing one global range. That independence is the entire value of a factory over a single hard-coded decorator: the same validation LOGIC gets reused with different CONFIGURATION each time it is called.

Treating the factory itself as if it were already a decorator

Wrong

python
def validate_range(minimum, maximum):
    def decorator(func):
        def wrapper(value):
            if not (minimum <= value <= maximum):
                raise ValueError("out of range")
            return func(value)
        return wrapper
    return decorator

@validate_range   # missing the call — (0, 100) never happened
def set_volume(level):
    return f"volume set to {level}"

set_volume(80)   # TypeError

Better

python
@validate_range(0, 100)   # called first, THEN used as the decorator
def set_volume(level):
    return f"volume set to {level}"

set_volume(80)

What you see: TypeError: decorator() missing 1 required positional argument: 'func' — @validate_range applies validate_range ITSELF (which expects minimum and maximum) directly to set_volume, treating set_volume as if it were the minimum argument.

Why: validate_range is a factory, not a decorator — it must be CALLED with its configuration arguments first, and only the function that call returns is a valid decorator. Writing @validate_range without the parentheses skips that call entirely and hands the factory function the decorated function as if it were minimum, which the factory's own signature was never built to accept that way.

One factory, called twice, makes two independent decorators

validate_range(minimum, maximum)

the factory — a plain function

validate_range(0, 100)

called once — returns decorator #1

validate_range(0, 5)

called again — returns a SEPARATE decorator #2

  1. validate_range(minimum, maximum) — the factory — a plain function
  2. validate_range(0, 100) — called once — returns decorator #1
  3. validate_range(0, 5) — called again — returns a SEPARATE decorator #2

One factory, several independently configured decorators

One factory, several independently configured decorators
CallDecorator producedEnforces
validate_range(0, 100)a decorator closing over minimum=0, maximum=100value must be 0–100
validate_range(0, 5)a SEPARATE decorator closing over minimum=0, maximum=5value must be 0–5

Together

python
def validate_range(minimum, maximum):
    def decorator(func):
        def wrapper(value):
            if not (minimum <= value <= maximum):
                raise ValueError(f"{value} not in range [{minimum}, {maximum}]")
            return func(value)
        return wrapper
    return decorator

@validate_range(0, 100)
def set_volume(level):
    return f"volume set to {level}"

@validate_range(0, 5)
def set_priority(level):
    return f"priority set to {level}"

Remember: A decorator factory is a plain function returning a decorator — call it with config first; each call produces an independent decorator.

See also: decorators with arguments · function decorators · closures

Advertisement

Decorators in classes

Applying the same mechanism to a method or a whole class, and stacking more than one at once.

Method decorators

standardintermediate

A decorator works on a method the same way it works on a plain function, as long as wrapper accepts *args, **kwargs — self simply arrives as the first positional argument, like it does for any instance method call.

Think of it as

Python never treats self specially at the syntax level — instance.method(x) is just method(instance, x) with nicer spelling. A decorator built for plain functions already forwards *args, **kwargs, so self rides along as args[0] without the decorator needing to know a method is involved at all.

python
def log_call(method):
    def wrapper(self, *args, **kwargs):
        print(f"{type(self).__name__}.{method.__name__} called")
        return method(self, *args, **kwargs)
    return wrapper

class Account:
    @log_call
    def deposit(self, amount):
        self.balance += amount
        return self.balance

What we're doing: Decorate an instance method with a logging decorator, and confirm self reaches both the decorator and the original method correctly.

account_logging.pypython
import functools

def log_call(method):
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        print(f"{type(self).__name__}.{method.__name__} called")
        return method(self, *args, **kwargs)
    return wrapper


class Account:
    def __init__(self, balance):
        self.balance = balance

    @log_call
    def deposit(self, amount):
        self.balance += amount
        return self.balance


acct = Account(100)
print(acct.deposit(50))
5
wrapper(self, *args, **kwargs) explicitly names self as the first parameter — it arrives as acct, the instance deposit was called on.
6
type(self).__name__ reads the instance's class name for the log line — self behaves like any other argument once inside wrapper.
7
method(self, *args, **kwargs) forwards self on to the real deposit, exactly as acct.deposit(50) would have without the decorator.
Output
Account.deposit called
150

Why this works: acct.deposit(50) is really Account.deposit(acct, 50), and Account.deposit is now wrapper — so self is bound to acct automatically by the normal method-lookup rules, arriving inside wrapper exactly like it would inside any undecorated method. Nothing about decoration changes how Python binds self; the decorator just sits between the call and the original method.

A decorator that checks argument count without accounting for self

Wrong

python
def requires_exactly_one_arg(func):
    def wrapper(*args, **kwargs):
        if len(args) != 1:
            raise TypeError(f"{func.__name__} expects exactly 1 argument, got {len(args)}")
        return func(*args, **kwargs)
    return wrapper

class Widget:
    @requires_exactly_one_arg
    def resize(self, factor):
        return factor

w = Widget()
w.resize(2)   # TypeError — self silently counts as an argument too

Better

python
def requires_exactly_one_arg(func):
    def wrapper(self, *args, **kwargs):   # self excluded from the count
        if len(args) != 1:
            raise TypeError(f"{func.__name__} expects exactly 1 argument, got {len(args)}")
        return func(self, *args, **kwargs)
    return wrapper

class Widget:
    @requires_exactly_one_arg
    def resize(self, factor):
        return factor

w = Widget()
w.resize(2)

What you see: TypeError: resize expects exactly 1 argument, got 2 — self was counted alongside factor, even though the caller only wrote one argument, w.resize(2).

Why: A decorator written and tested only against plain functions has no reason to expect a hidden first argument — once it decorates a method, self shows up inside *args exactly like any other positional argument, silently breaking any logic that assumes args lines up one-to-one with what the caller wrote.

Remember: A decorator built for plain functions decorates a method unchanged, as long as wrapper forwards *args, **kwargs — self just arrives as the first one.

See also: function decorators · class decorators · instance methods

Class decorators

standardintermediate

A class decorator takes a class instead of a function, and must return a class — usually the same one, modified. @add_repr above class Point: is exactly Point = add_repr(Point), same rule as a function decorator.

Think of it as

A class decorator edits the blueprint itself, once, before any object is built from it — unlike a method decorator, which wraps behaviour per-call. Attach a method to the class, register the class somewhere, or validate its shape, all before a single instance exists.

python
def add_repr(cls):
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({fields})"
    cls.__repr__ = __repr__
    return cls          # must return a class

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

What we're doing: Attach an auto-generated __repr__ to a class using a class decorator, and confirm it works on real instances.

add_repr.pypython
def add_repr(cls):
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({fields})"
    cls.__repr__ = __repr__
    return cls


@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


p = Point(1, 2)
print(repr(p))
print(Point.__name__)
1
add_repr(cls) receives the Point class OBJECT itself, not an instance — no Point has been created yet at this point.
5
cls.__repr__ = __repr__ attaches a new method directly onto the class, mutating it in place.
6
return cls hands the same (now-modified) class back — a class decorator must return a class, exactly like a function decorator must return a callable.
9
@add_repr runs add_repr(Point) once, immediately after the class body finishes, and rebinds the name Point to whatever it returns.
Output
Point(x=1, y=2)
Point

Why this works: p = Point(1, 2) builds an instance from the ALREADY-decorated Point, so its __repr__ is the one add_repr attached — repr(p) calls it and reads p.__dict__ to format the fields. Point.__name__ still reads "Point" because add_repr returned the same class object it received, just with one extra method, not a brand-new class.

Forgetting to return the class from a class decorator

Wrong

python
def add_repr(cls):
    def __repr__(self):
        return f"{cls.__name__}(...)"
    cls.__repr__ = __repr__
    # BUG: no return statement — falls off the end, returning None

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)   # TypeError — Point is now None

Better

python
def add_repr(cls):
    def __repr__(self):
        return f"{cls.__name__}(...)"
    cls.__repr__ = __repr__
    return cls   # the class decorator's contract: always return a class

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)

What you see: TypeError: 'NoneType' object is not callable — Point(1, 2) tries to call None, because @add_repr rebound the name Point to add_repr's return value, and a function with no return statement returns None.

Why: @add_repr above class Point: is exactly Point = add_repr(Point) — whatever add_repr returns becomes the new meaning of the name Point. A class decorator that mutates cls in place still has to explicitly return cls at the end, or the class itself is silently replaced with None.

Remember: @my_decorator above class Name: is Name = my_decorator(Name) — the decorator receives the class object and must return a class back.

See also: function decorators · method decorators · classes and objects

Stacked decorators

coreintermediate

Stacking @bold above @italic above a def applies italic first, then bold — bottom-up, at decoration time. Calling the result then runs bold's wrapper first, which calls italic's wrapper, which calls the original — outer-in, at call time.

Think of it as

Stacked decorators are nested gift boxes, wrapped from the inside out. italic (closest to def) wraps the plain function first; bold then wraps that already-wrapped result. Opening the boxes later — calling the function — goes the other direction: bold's layer is opened first, which reveals italic's layer, which finally reveals the original.

python
@bold
@italic
def shout(text):
    return text.upper()

# equivalent to:
# shout = bold(italic(shout))

What we're doing: Prove stacked decorators apply bottom-up at decoration time by printing from each wrapper, then confirm swapping the stack changes both the print order and the output.

stacked_decorators.pypython
import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("applying bold wrapper")
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("applying italic wrapper")
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper


@bold
@italic
def shout(text):
    return text.upper()

print(shout("hi"))

print("--- swapped stack ---")

@italic
@bold
def shout2(text):
    return text.upper()

print(shout2("hi"))
18
@italic (closest to def) applies first: shout becomes italic(shout). @bold then applies to THAT result: shout becomes bold(italic(shout)).
22
Calling shout("hi") runs bold's wrapper first — it's now the outermost layer — which prints its line before calling into italic's wrapper.
27
Swapping which decorator is on top reverses BOTH the decoration order and the call order, and changes the final HTML nesting.
Output
applying bold wrapper
applying italic wrapper
<b><i>HI</i></b>
--- swapped stack ---
applying italic wrapper
applying bold wrapper
<i><b>HI</b></i>

Why this works: @bold\n@italic\ndef shout is exactly shout = bold(italic(shout)) — italic(shout) runs first and produces an intermediate wrapper, which bold then wraps again. That makes bold's wrapper the OUTERMOST layer, so calling shout("hi") reaches it first — "applying bold wrapper" prints before "applying italic wrapper" runs one call deeper. Swapping the stack to @italic over @bold reverses both the decoration order and the print order, and the output nesting flips from <b><i>...</i></b> to <i><b>...</b></i> to match.

Assuming stack order does not matter because "they are just decorators"

Wrong

python
@cache_result       # assumes: caching always happens LAST, safely
@require_auth        # assumes: auth is always checked first, either way
def get_user_data(user_id):
    return fetch_from_db(user_id)

# actually: decoration is bottom-up, so require_auth wraps get_user_data
# FIRST, and cache_result wraps THAT — meaning cache_result's wrapper
# runs before require_auth's, and a cached result can be returned to an
# unauthenticated caller without require_auth ever running for that call

Better

python
@require_auth         # outermost — runs FIRST on every call, unconditionally
@cache_result          # inner — only reached once auth has already passed
def get_user_data(user_id):
    return fetch_from_db(user_id)

What you see: No exception — the bug is silent. A cached response can bypass an auth check that the developer assumed always ran first, because the decorator that should gate access sits underneath, not on top.

Why: The decorator listed FIRST (closest to the top) becomes the OUTERMOST wrapper, and the outermost wrapper is the one that runs first on every call — anything that must run unconditionally, like an auth check, has to be on top of the stack, not just "present somewhere in it".

Decoration wraps bottom-up; calling unwraps top-down

@bold wrapper

outermost — applied LAST, runs FIRST

@italic wrapper

applied first, runs second

shout(text)

the original function, innermost

  1. @bold wrapper — outermost — applied LAST, runs FIRST
  2. @italic wrapper — applied first, runs second
  3. shout(text) — the original function, innermost

Stack order vs. what actually happens

Stack order vs. what actually happens
Written asEquivalent toRuns at call time
@bold\n@italic\ndef f(): ...f = bold(italic(f))bold\'s wrapper first, then italic\'s, then f
@italic\n@bold\ndef f(): ...f = italic(bold(f))italic\'s wrapper first, then bold\'s, then f

Together

python
@bold
@italic
def shout(text):
    return text.upper()

shout("hi")   # "<b><i>HI</i></b>"

Remember: Stacked decorators apply bottom-up (closest to def first) but the resulting wrappers run top-down at call time — the topmost decorator sees every call first.

See also: function decorators · functools wraps · decorators with arguments

Advertisement

Getting it right

Keeping a decorated function's identity intact, and building a decorator as a class instead of a closure.

functools.wraps

coreintermediate

Without @functools.wraps(func) on wrapper, a decorated function reports wrapper's own __name__ and __doc__ instead of the original's. @functools.wraps(func) copies them over and adds __wrapped__, pointing back at the real function.

Think of it as

A wrapper is a stand-in actor wearing the original function's name tag. Without functools.wraps, the stand-in shows up to every introspection call — help(), __name__, a debugger — wearing its OWN blank tag instead, and nobody watching can tell which function actually got called. functools.wraps copies the original's tag onto the stand-in.

python
import functools

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

What we're doing: Decorate the same function twice — once without functools.wraps and once with it — and compare __name__, __doc__ and __wrapped__ side by side.

wraps_comparison.pypython
import functools

def broken_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@broken_decorator
def calculate_total(items):
    """Sum up the price of every item."""
    return sum(items)


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


@fixed_decorator
def calculate_total2(items):
    """Sum up the price of every item."""
    return sum(items)


print("broken __name__:", calculate_total.__name__)
print("broken __doc__:", calculate_total.__doc__)
print("broken has __wrapped__:", hasattr(calculate_total, "__wrapped__"))

print("fixed __name__:", calculate_total2.__name__)
print("fixed __doc__:", calculate_total2.__doc__)
print("fixed __wrapped__ name:", calculate_total2.__wrapped__.__name__)
4
broken_decorator's wrapper has no @functools.wraps — it keeps its OWN __name__ ("wrapper") and __doc__ (None) after decoration.
16
@functools.wraps(func) runs before wrapper is even defined below it — it decorates wrapper itself, copying func's identity onto it.
24
calculate_total2 goes through the identical decoration pattern, but with @functools.wraps present.
Output
broken __name__: wrapper
broken __doc__: None
broken has __wrapped__: False
fixed __name__: calculate_total2
fixed __doc__: Sum up the price of every item.
fixed __wrapped__ name: calculate_total2

Why this works: Both decorators replace calculate_total/calculate_total2 with wrapper, so without any correction, __name__ and __doc__ read straight off wrapper's own definition — "wrapper" and None. @functools.wraps(func), applied as a decorator to wrapper itself, explicitly copies func.__name__, func.__doc__ and more onto wrapper before it replaces the original, and additionally sets wrapper.__wrapped__ = func — which is why calculate_total2.__wrapped__.__name__ correctly reports the real function's name even though calculate_total2 itself IS wrapper.

Forgetting functools.wraps breaks tools that rely on __name__ or __doc__

Wrong

python
def logged(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@logged
def calculate_total(items):
    """Sum up the price of every item."""
    return sum(items)

help(calculate_total)   # shows "wrapper(*args, **kwargs)" — not calculate_total's real signature or docstring
print(calculate_total.__name__)   # 'wrapper' — a traceback would show this name too

Better

python
import functools

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

@logged
def calculate_total(items):
    """Sum up the price of every item."""
    return sum(items)

help(calculate_total)   # shows calculate_total's real docstring and name
print(calculate_total.__name__)   # 'calculate_total'

What you see: help(), a debugger, or a traceback all identify the function as 'wrapper' instead of 'calculate_total' — every decorated function in the codebase looks identical from the outside, with no docstring.

Why: wrapper is a completely ordinary function definition with its own __name__ and __doc__ — nothing links it back to func automatically. Any tool that introspects a function (help, logging, a debugger, Sphinx) reads wrapper's own metadata unless @functools.wraps(func) explicitly copies func's over first.

@functools.wraps(func) copies identity onto wrapper

func

__name__, __doc__ — the real identity

@functools.wraps(func)

copies them onto wrapper, adds __wrapped__

wrapper

now reports func's name and docstring as its own

  1. func — __name__, __doc__ — the real identity
  2. @functools.wraps(func) — copies them onto wrapper, adds __wrapped__
  3. wrapper — now reports func's name and docstring as its own

Broken vs. fixed decorator, same original function

Broken vs. fixed decorator, same original function
Attribute read on the decorated functionWithout functools.wrapsWith functools.wraps
__name__'wrapper''calculate_total'
__doc__None (wrapper has no docstring)the original function's docstring
__wrapped__AttributeError — does not existthe original, undecorated function object

Together

python
import functools

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

@fixed_decorator
def calculate_total(items):
    """Sum up the price of every item."""
    return sum(items)

print(calculate_total.__name__)
print(calculate_total.__doc__)
print(calculate_total.__wrapped__.__name__)

Remember: @functools.wraps(func) on wrapper copies __name__/__doc__/__wrapped__ from the original — skip it and the decorated function looks like "wrapper" everywhere.

See also: function decorators · callable objects · stacked decorators

Callable objects

standardintermediate

A class with __call__(self, ...) can decorate a function too — @CountCalls above def f(): runs CountCalls(f), and the resulting instance stands in for f. __call__ runs on every call, with self able to hold state as ordinary attributes.

Think of it as

A closure-based decorator hides its state inside variables captured by a nested function — reachable only through the wrapper itself. A class-based decorator puts the exact same state on self instead, as ordinary, directly-inspectable attributes like instance.call_count — the two approaches store the same information, just in different places.

python
import functools

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

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        return self.func(*args, **kwargs)

@CountCalls
def say_hello(name):
    return f"Hello, {name}"

What we're doing: Write a decorator as a class instead of a nested function, and confirm its instance both decorates correctly and exposes call_count as a plain, readable attribute.

count_calls.pypython
import functools

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

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"{self.func.__name__} has been called {self.call_count} time(s)")
        return self.func(*args, **kwargs)


@CountCalls
def say_hello(name):
    return f"Hello, {name}"


print(say_hello("Ana"))
print(say_hello("Ben"))
print(say_hello.call_count)
print(say_hello.__name__)
4
@CountCalls runs CountCalls(say_hello) — __init__ receives the original function and stores it on self.func, alongside self.call_count = 0.
5
functools.update_wrapper(self, func) copies __name__/__doc__ onto the INSTANCE self — the class-based equivalent of @functools.wraps.
9
__call__ makes the CountCalls instance itself callable — say_hello("Ana") is really say_hello.__call__("Ana"), since say_hello now IS a CountCalls instance.
20
say_hello.call_count reads self.call_count directly, as a plain attribute — no closure or special access needed to see the decorator's internal state.
Output
say_hello has been called 1 time(s)
Hello, Ana
say_hello has been called 2 time(s)
Hello, Ben
2
say_hello

Why this works: @CountCalls above say_hello rebinds the name say_hello to a CountCalls instance, exactly the way any decorator rebinds a name — the only difference is that this replacement is an object with __call__, not a nested function. Every call to say_hello(...) therefore triggers __call__, which increments self.call_count on that same instance before forwarding to self.func — and because call_count is a normal instance attribute, it is readable from outside as say_hello.call_count with no special access.

Forgetting __call__ makes the decorated name unusable as a function

Wrong

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.call_count = 0
    # no __call__ defined

@CountCalls
def say_hello(name):
    return f"Hello, {name}"

say_hello("Ana")   # TypeError — the instance isn't callable

Better

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):   # required for the instance to act as a decorator
        self.call_count += 1
        return self.func(*args, **kwargs)

@CountCalls
def say_hello(name):
    return f"Hello, {name}"

say_hello("Ana")

What you see: TypeError: 'CountCalls' object is not callable — say_hello is now a CountCalls instance, and instances are only callable if their class defines __call__.

Why: @CountCalls replaces say_hello with a CountCalls instance, not a function — the only way an object can be used as obj(...) is for its class to implement __call__. Without it, __init__ still runs fine at decoration time, but the resulting object has no way to stand in for the original callable function.

Remember: A class with __call__ can decorate a function — the instance replaces it, and state lives as ordinary, readable attributes on self.

See also: function decorators · functools wraps · call dunder

Advertisement