Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 4 – Functions, scope, closures and decorators

Decorators: wrapping functions cleanly

See what @decorator really does, write wrappers that keep the name and docstring with functools.wraps, add arguments, stack decorators and decorate classes.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Explain decorator syntax as rebinding a name to a wrapper
  • Write wrappers that pass every argument on and keep metadata with functools.wraps
  • Write decorators that take arguments
  • Predict the order in which stacked decorators are applied and run
  • Write a class decorator that changes and returns the class

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain decorator syntax as rebinding a name to a wrapper
  • Write wrappers that pass every argument on and keep metadata with functools.wraps
  • Write decorators that take arguments
  • Predict the order in which stacked decorators are applied and run
  • Write a class decorator that changes and returns the class

Before you start

On this page

Adding timing to 15 functions by hand means pasting the same four lines into each one, and taking them out again later. A decorator: A function that takes a function and returns the one its name should refer to, usually a wrapper. @timed above def report(): has the same effect as report = timed(report) after it. does it once: it is a function that takes a function and returns a replacement for it, usually a wrapper: A function that calls another function and passes on the arguments it received, usually as func(*args, **kwargs), doing extra work such as logging before or after the call. More in “*args, **kwargs and argument unpacking” that runs some extra code before and after calling the original. Writing @timed on the line above def report(): has the same effect as report = timed(report) after it: Python creates the function, passes it to timed, and binds the name report to whatever timed returns.

Below you will watch a name being rebound to a wrapper, follow one call through a wrapper, keep the original’s name and docstring with functools.wraps, give a decorator arguments, stack two of them, see a decorator do its work as a module loads, and decorate a method and a class.

@decorator rebinds the name to what the decorator returns

A decorator needs nothing new: it is a function that takes a function, defines a wrapper inside itself and returns it. The wrapper is a closure, so it remembers func:

A decorator written and applied by hand, then with @ Python · by_hand.py
def shout(func):
    """Take a function and return a new one that upper-cases its result."""
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper


def greet(name):
    """Return a greeting."""
    return f"hello, {name}"


loud_greet = shout(greet)   # wrapping by hand: greet itself is unchanged
print(greet("Asha"), "|", loud_greet("Asha"))


@shout                      # the same as farewell = shout(farewell) after the def
def farewell(name):
    """Say goodbye."""
    return f"goodbye, {name}"


print(farewell("Ravi"))
print(farewell.__name__, "|", farewell.__doc__)   # the wrapper's name, and no docstring

Output

hello, Asha | HELLO, ASHA
GOODBYE, RAVI
wrapper | None

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 by_hand.py

  • shout(greet) returns a new function and leaves greet unchanged, so both can be called.
  • @shout above def farewell does the same wrapping at once and binds the name farewell to the wrapper.
  • The last line shows the price of wrapping: farewell.__name__ is now wrapper and its docstring is gone, because the name farewell refers to the wrapper.

Step through the same idea and watch the names. greet is rebound by hand, and farewell by the @ line, which Python carries out in four small steps:

Interactive · line by line Wrapping greet by hand, then farewell with @shout Step through the program and watch the names greet and farewell. The lit line has just run; the variables show what each name refers to.

Program · rebinding.py

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


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


print(greet("Asha"))
greet = shout(greet)
print(greet("Asha"))


@shout
def farewell(name):
    return f"goodbye, {name}"


print(farewell("Ravi"))

Variables

After line 21: shout is shout(func); greet is wrapper(*args, **kwargs); farewell is wrapper(*args, **kwargs).shoutshout(func)greetwrapper(*args, **kwargs)farewellwrapper(*args, **kwargs)
  • changed in this step

Printed so far

hello, Asha
HELLO, ASHA
GOODBYE, RAVI

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 11 steps are listed below.

Line run 21
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. def shout(func): makes the function shout. Its body runs only when it is called.
  3. Line 7. def greet(name): makes the function greet. Its body runs only when it is called.
  4. Line 11. print(greet("Asha")) prints hello, Asha.
  5. Line 12. greet = shout(greet) sets greet to wrapper(*args, **kwargs) (it was greet(name)). Key point shout(greet) returns its inner function, wrapper, and the assignment rebinds the name greet to it. The original function is not gone; the wrapper holds it as func.
  6. Line 13. print(greet("Asha")) prints HELLO, ASHA.
  7. Line 16. Line 16 runs. For a decorated def, Python first works out the decorator expression on the @ line, here the function shout.
  8. Line 17. def farewell(name): makes the function farewell. Its body runs only when it is called. Then the def makes the function object. It is not bound to the name farewell yet.
  9. Line 16. Line 16 runs. Now Python calls shout with the new function. The undecorated function is never bound to the name farewell.
  10. Line 17. def farewell(name): makes the function farewell. Its body runs only when it is called. Key point The name farewell is bound to what shout returned, its wrapper, exactly as farewell = shout(farewell) would bind it after a plain def.
  11. Line 21. print(farewell("Ravi")) prints GOODBYE, RAVI. That was the last line, so the program ends. Both names refer to wrappers now. Each call goes through the wrapper, which calls the original and upper-cases its result.

The decorated def worked out shout, made the function, called shout with it and bound farewell to the result. The language reference describes the @ line in just this way, as farewell = shout(farewell) after a plain def, except that the original function is never bound to the name farewell at all.

A wrapper passes every argument on and returns the result

The wrapper in shout() accepts *args and **kwargs and passes them on, so it works for a function with any parameters, and it returns the original’s result. Follow one call through it, from your code to the original and back:

Interactive · message by message One call of farewell("Ravi") through its wrapper Step through the call. Each arrow is a call with its arguments, or the value a call returns.
One call of farewell("Ravi") through its wrapper, step 5 of 5. Messages so far: Your code to wrapper: farewell("Ravi"); wrapper to the original: func("Ravi"); the original to wrapper: 'goodbye, Ravi'; wrapper to Your code: 'GOODBYE, RAVI'.Your codewrapperwhat farewell namesthe originalthe def you wrotefarewell("Ravi")func("Ravi")'goodbye, Ravi''GOODBYE, RAVI'

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The call. farewell("Ravi") calls the wrapper, because that is what the name farewell refers to after @shout.
  2. Arguments collected. The wrapper's args collects the positional arguments, ('Ravi',), and *kwargs the keyword ones, here none, so it accepts whatever the original accepts.
  3. Passed on unchanged. func(args, *kwargs) spreads them back out, so the original receives exactly the call it would have received without the decorator.
  4. The original's result. The original returns 'goodbye, Ravi' to the wrapper, which can work on the result before handing it back.
  5. Returned, changed. The wrapper returns the result upper-cased, 'GOODBYE, RAVI', which the program prints. Without its return statement, the call would give the caller None instead.

A wrapper that drops the result

def wrapper(*args, **kwargs): func(*args, **kwargs) calls the original and throws its result away, so every call of the decorated function returns None. Write return func(*args, **kwargs).

A timing decorator is the classic first use: start a clock, call the function, stop the clock, report. Putting the report in a finally block makes it run even when the function raises:

@timed with time.perf_counter() Python · timed.py
import functools
import time


def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:   # runs even when func raises
            elapsed_ms = (time.perf_counter() - start) * 1000
            print(f"{func.__name__} took {elapsed_ms:.2f} ms")
    return wrapper


@timed
def sum_of_squares(n):
    return sum(i * i for i in range(n))


print(sum_of_squares(200_000))

Output

sum_of_squares took 8.84 ms
2666646666700000

This output changes from run to run: it measures time, which differs from one run to the next and from one computer to another; the computer of this run is named below the output.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timed.py

time.perf_counter() is the clock for measuring short durations: it has the highest available resolution, and only the difference between two readings means anything. The time line comes before the result, because the wrapper prints it before print() gets the return value.

Checkpoint

Check: @decorator as rebinding the name · A wrapper that passes everything on

  1. Question 1 of 2 What does this program print?

    Read the code, then choose one answer.

    def tag(func):
        return "tagged"
    
    @tag
    def save():
        return "saved"
    
    print(save)
    A hint

    What does Python bind to the name save after the def?

    Show the answer: Question 1 of 2

    Answer: tagged

    @tag above def save means save = tag(save) after the def. tag returns "tagged", so the name save refers to that string. A decorator's return value is the whole story, which is why a decorator that forgets to return leaves None behind.

  2. Question 2 of 2 Which wrapper works for any function it wraps, whatever arguments the function takes, and keeps its result?

    Choose one answer.

    Show the answer: Question 2 of 2

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

    A general wrapper collects whatever the call passes with args and *kwargs, spreads it back out with the same stars when it calls the original, and returns the original's result.

functools.wraps keeps the name, docstring and signature

functools.wraps(func) is itself a decorator, applied to the wrapper. It copies the original’s identity onto the wrapper and records the original in a new attribute, __wrapped__. Step through one wrapper made without it and one made with it, and compare a and b:

Interactive · line by line The same wrapper without and with functools.wraps Step through the program and compare a and b in the variables. Each value shows the name and the signature that Python reports for it.

Program · wraps_compare.py

import functools


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


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


def farewell(name, punctuation="!"):
    """Say goodbye."""
    return f"goodbye, {name}{punctuation}"


a = plain(farewell)
b = kept(farewell)
print(a.__name__, "|", a.__doc__)
print(b.__name__, "|", b.__doc__)
print(b.__wrapped__ is farewell)

Variables

After line 26: plain is plain(func); kept is kept(func); farewell is farewell(name, punctuation='!'); a is wrapper(*args, **kwargs); b is farewell(name, punctuation='!').plainplain(func)keptkept(func)farewellfarewell(name, punctuation='!')awrapper(*args, **kwargs)bfarewell(name, punctuation='!')
  • changed in this step

Printed so far

wrapper | None
farewell | Say goodbye.
True

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 10 steps are listed below.

Line run 26
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. import functools loads functools.
  3. Line 4. def plain(func): makes the function plain. Its body runs only when it is called.
  4. Line 10. def kept(func): makes the function kept. Its body runs only when it is called.
  5. Line 17. def farewell(name, punctuation='!'): makes the function farewell. Its body runs only when it is called.
  6. Line 22. a = plain(farewell) sets a to wrapper(*args, **kwargs). a is the wrapper made without wraps. Its name is wrapper and its signature (*args, **kwargs), which is all repr(), a log line or help() could show.
  7. Line 23. b = kept(farewell) sets b to farewell(name, punctuation='!'). Key point b is a wrapper too, but wraps copied the name and the docstring onto it, and inspect.signature() follows its __wrapped__ to the original's parameters.
  8. Line 24. print(a.__name__, "|", a.__doc__) prints wrapper | None.
  9. Line 25. print(b.__name__, "|", b.__doc__) prints farewell | Say goodbye..
  10. Line 26. print(b.__wrapped__ is farewell) prints True. That was the last line, so the program ends. b.__wrapped__ is the original farewell, so a test can still reach the undecorated function.

Here is shout with wraps, on the farewell of the first example:

The same decorator with functools.wraps Python · keep_metadata.py
import functools
import inspect


def shout(func):
    @functools.wraps(func)   # copy the name, docstring and more from func to wrapper
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper


@shout
def farewell(name, punctuation="!"):
    """Say goodbye."""
    return f"goodbye, {name}{punctuation}"


print(farewell("Ravi"))
print(farewell.__name__, "|", farewell.__doc__, "|", farewell.__qualname__)
print(inspect.signature(farewell))   # follows __wrapped__ to the original
print(farewell.__wrapped__("Ravi"))  # the undecorated function is still reachable
print(functools.WRAPPER_ASSIGNMENTS)

Output

GOODBYE, RAVI!
farewell | Say goodbye. | farewell
(name, punctuation='!')
goodbye, Ravi!
('__module__', '__name__', '__qualname__', '__doc__', '__annotate__', '__type_params__')

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 keep_metadata.py

The decorated farewell now reports its own name, qualified name and docstring, which help(), repr(), documentation tools and log lines that print func.__name__ all use. inspect.signature() follows __wrapped__ to the original and shows (name, punctuation='!') instead of the wrapper’s (*args, **kwargs), and farewell.__wrapped__ lets a test call the undecorated function directly.

Key idea

Put @functools.wraps(func) on every wrapper: the decorated function then keeps its name, docstring and signature, and __wrapped__ leads back to the original.

The last line of the output lists the attributes wraps copies, functools.WRAPPER_ASSIGNMENTS, and one of them is new:

Version note

One entry changed in Python 3.14: __annotate__, the function that works out a function’s annotations when they are first read, took the place of the finished __annotations__ dictionary, because annotations are evaluated lazily since Python 3.14.

A decorator with arguments is a function that returns a decorator

@retry(times=3) has parentheses, so Python first calls retry(times=3) and uses what that call returns as the decorator. A decorator with arguments: A function that takes settings and returns a decorator, such as retry(times=3). Python calls it first, at the def, and applies the decorator it returns to the function. is therefore one level deeper: a function that takes the arguments and returns a decorator, which takes the function and returns a wrapper. Here @retry(times=3) gives a stock lookup that fails at random up to three attempts:

@retry(times=3) around a flaky call Python · retry.py
import functools
import random


def retry(times=3, on=(ConnectionError,)):
    """A decorator factory: retry(times=3) returns the decorator."""
    def decorate(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except on as err:
                    print(f"  attempt {attempt} of {times} failed: {err}")
                    if attempt == times:
                        raise   # give up: the caller sees the last error
        return wrapper
    return decorate


network = random.Random(1)   # seeded, so the "network" fails the same way every run


@retry(times=3)
def fetch_stock(item):
    if network.random() < 0.5:
        raise ConnectionError("no answer from the warehouse")
    return {"tea": 12, "rice": 40}.get(item, 0)


for item in ["tea", "rice", "salt"]:
    try:
        print(item, fetch_stock(item))
    except ConnectionError:
        print(item, "unknown: gave up")
print(fetch_stock.__name__)

Output

  attempt 1 of 3 failed: no answer from the warehouse
tea 12
rice 40
  attempt 1 of 3 failed: no answer from the warehouse
  attempt 2 of 3 failed: no answer from the warehouse
  attempt 3 of 3 failed: no answer from the warehouse
salt unknown: gave up
fetch_stock

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 retry.py

The “network” is a random number generator with a fixed seed, so it fails the same way on every run. tea failed once and succeeded on the second attempt, rice succeeded at once, and salt failed three times, after which the wrapper re-raised the last ConnectionError for the caller to handle. Step through the three levels:

Interactive · stage by stage The three levels of @retry(times=3) Step through what Python does with the decorator line and the def, then with each call of fetch_stock().
The three levels of @retry(times=3), step 5 of 5. Showing retry(times=3) runs, it returns decorate, decorate(fetch_stock), it returns wrapper, each call runs wrapper. In focus: each call runs wrapper.retry(times=3) runsit stores times and onit returns decoratethe real decoratordecorate(fetch_stock)receives the functionit returns wrappernow named fetch_stockeach call runs wrapperup to 3 attempts

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The parentheses first. @retry(times=3) has parentheses, so Python first calls retry(times=3). This call happens once, when the def runs, not on every call of fetch_stock().
  2. A decorator comes back. retry() returns decorate, a function that takes a function. That is the decorator Python actually applies.
  3. The function arrives. Python makes the function fetch_stock and passes it to decorate(), which defines wrapper around it.
  4. The name is rebound. decorate() returns wrapper, and the name fetch_stock is bound to it. wrapper can still read times and on from retry's call, and func from decorate's.
  5. Every call. fetch_stock("salt") runs wrapper, which tries up to times = 3 attempts and re-raises the last ConnectionError when all of them fail, as the run shows for salt.

retry stores the settings, decorate receives the function, and wrapper runs on every call, reading times and on from the enclosing scopes. Real retry decorators also wait between attempts, usually longer each time, and they retry only errors that are likely to pass, such as a timeout. Never retry an operation that is not safe to repeat, such as a payment, without a way to recognise the repeat.

Leaving out the parentheses

@retry without them passes the function itself to retry as times, so the name ends up bound to decorate, and calling fetch_stock("tea") returns yet another function instead of a result. Write @retry() or @retry(times=3).

Checkpoint

Check: Keeping metadata with functools.wraps · Decorators that take arguments

  1. Question 1 of 2 What does this program print?

    Read the code, then choose one answer.

    import functools
    
    def logged(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    
    @logged
    def total(prices):
        """Add up the prices."""
        return sum(prices)
    
    print(total.__name__, total.__doc__)
    Show the answer: Question 1 of 2

    Answer: total Add up the prices.

    The name total refers to the wrapper, but functools.wraps copied __module__, __name__, __qualname__, __doc__ and a few more attributes from the original, so the wrapper reports the original's name and docstring.

  2. Question 2 of 2 A program defines fetch_stock under @retry(times=3) and then calls fetch_stock() three times. How many times is retry() itself called?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Once, when the def runs, to make the decorator

    @retry(times=3) is a call: it runs once, with the def, and returns decorate. decorate runs once too, with the new function, and returns wrapper. Only wrapper runs on every call of fetch_stock().

Stacked decorators apply from the bottom up and run from the top down

Several decorators can sit above one def. The one nearest the function is applied first, and each of the others wraps the result of the one below it:

Two decorators on one function Python · stacking.py
import functools


def tag(label):
    def decorate(func):
        print(f"applying {label} to {func.__name__}")
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"  enter {label}")
            result = func(*args, **kwargs)
            print(f"  leave {label}")
            return result
        return wrapper
    return decorate


@tag("outer")
@tag("inner")   # applied first: it is closest to the def
def work():
    print("    working")
    return 42


print("--- now calling work()")
print(work())

Output

applying inner to work
applying outer to work
--- now calling work()
  enter outer
  enter inner
    working
  leave inner
  leave outer
42

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stacking.py

The name work is bound to the outer wrapper, which calls the inner wrapper, which calls the original function body.the name workouter wrapperfrom tag("outer")inner wrapperfrom tag("inner")the original work()the body you wroteis bound tocallscalls

What two stacked decorators leave behind

Text description of the diagram

The diagram shows four boxes from top to bottom, joined by arrows.

  • At the top is the name work. After the decorators have run it is bound to the outer wrapper, the function that tag("outer") returned.
  • The outer wrapper calls the inner wrapper, the function that tag("inner") returned.
  • The inner wrapper calls the original work(), the body written under the def.

Python applied the decorators from the bottom up: first tag("inner") wrapped the original function, then tag("outer") wrapped that result. A call therefore travels top-down through the layers: outer, inner, original; and the results come back up in the opposite order.

The two “applying” lines appear before any call, inner first, because decorators run when the def statement runs. Written out by hand, the two @ lines are work = tag("outer")(tag("inner")(work)), and the innermost call is made first. The name work ends up bound to the outer wrapper, as the diagram shows, so a call travels from the outside in. Step through it:

Interactive · message by message One call of work() through two stacked wrappers Step through the call after both decorators have run. Each arrow is a call, or the value a call returns.
One call of work() through two stacked wrappers, step 6 of 6. Messages so far: outer wrapper to inner wrapper: func(); inner wrapper to the body of work(): func(); the body of work() to inner wrapper: 42; inner wrapper to outer wrapper: 42.outer wrapperwhat work namesinner wrapperfrom tag("inner")the body of work()func()func()4242

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Already applied. Both decorators ran when the def ran, nearest first: tag("inner") wrapped the original, then tag("outer") wrapped that. The name work refers to the outer wrapper.
  2. Enter outer. work() calls the outer wrapper, which prints "enter outer" and calls the function it wraps, the inner wrapper.
  3. Enter inner. The inner wrapper prints "enter inner" and calls the function it wraps, the original body.
  4. The body runs. The body prints "working" and returns 42 to the inner wrapper.
  5. Leave inner. The inner wrapper prints "leave inner" and returns 42 to the outer wrapper.
  6. Leave outer. The outer wrapper prints "leave outer" and returns 42, which print() shows. The outer wrapper started first and finished last.

The outer wrapper starts first and finishes last. Order changes meaning: a timing decorator placed above a retry decorator measures all the attempts together; placed below it, it times every attempt separately.

Decorators run once, when the def runs

The decorator is called once, when Python executes the def, which for a module is when it is first imported. A decorator can use that moment to record the function somewhere and return it unchanged. Step through a program that registers two commands, and watch COMMANDS:

Interactive · line by line A decorator that registers commands as the module runs Step through the program and watch COMMANDS. No command is called until the last line.

Program · registry.py

COMMANDS = {}


def command(name):
    """Register a function under a name, and return it unchanged."""
    def register(func):
        COMMANDS[name] = func
        return func
    return register


print("before the defs:", COMMANDS)


@command("add")
def add(a, b):
    return a + b


@command("mul")
def multiply(a, b):
    return a * b


print("after the defs:", sorted(COMMANDS))   # filled while the module ran, before any call
print(COMMANDS["mul"](6, 7), multiply(2, 3))

Variables

After line 26: COMMANDS is {'add': add(a, b), 'mul': multiply(a, b)}; command is command(name); add is add(a, b); multiply is multiply(a, b).COMMANDS'add':add(a, b)'mul':multiply(a, b)commandcommand(name)addadd(a, b)multiplymultiply(a, b)
  • changed in this step

Printed so far

before the defs: {}
after the defs: ['add', 'mul']
42 6

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 14 steps are listed below.

Line run 26
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. COMMANDS = {} makes COMMANDS a dict with 0 keys.
  3. Line 4. def command(name): makes the function command. Its body runs only when it is called.
  4. Line 12. print("before the defs:", COMMANDS) prints before the defs: {}.
  5. Line 15. Line 15 runs. command("add") runs at once and returns register, which is the decorator Python will apply.
  6. Line 16. def add(a, b): makes the function add. Its body runs only when it is called.
  7. Line 15. Line 15 runs. Key point register(add) has put the new function into COMMANDS and handed it back unchanged. Nothing has called add yet.
  8. Line 16. def add(a, b): makes the function add. Its body runs only when it is called.
  9. Line 20. Line 20 runs.
  10. Line 21. def multiply(a, b): makes the function multiply. Its body runs only when it is called.
  11. Line 20. Line 20 runs.
  12. Line 21. def multiply(a, b): makes the function multiply. Its body runs only when it is called. multiply is registered under "mul" the same way, so COMMANDS is full before any command has run.
  13. Line 25. print("after the defs:", sorted(COMMANDS)) prints after the defs: ['add', 'mul'].
  14. Line 26. print(COMMANDS["mul"](6, 7), multiply(2, 3)) prints 42 6. That was the last line, so the program ends. The dictionary filled while the module itself ran. A decorator's work at definition time happens when a module is imported.

The COMMANDS dictionary was empty before the def statements and full after them, before any command was called. Press Edit, add a third command, @command("sub") above a def subtract(a, b): that returns a - b, and run it: the line printed after the defs lists three names, and still nothing has been called. Web frameworks use this pattern to connect URLs to functions, and command-line tools to connect commands to them. The other side of it: whatever a decorator does at definition time happens on import, so a slow or failing decorator slows or breaks the import of the whole module.

Checkpoint

Check: Stacked decorators · Decorators run at definition time

  1. Question 1 of 2 What does this program print?

    Read the code, then choose one answer.

    def add_a(func):
        def wrapper():
            return func() + "a"
        return wrapper
    
    def add_b(func):
        def wrapper():
            return func() + "b"
        return wrapper
    
    @add_a
    @add_b
    def word():
        return "x"
    
    print(word())
    A hint

    Which wrapper adds its letter first, the one nearest the body or the outer one?

    Show the answer: Question 1 of 2

    Answer: xba

    Decorators are applied from the bottom up, so the name word refers to add_a's wrapper around add_b's wrapper. A call runs from the outside in, and the results come back from the inside out: "x", then "xb", then "xba".

  2. Question 2 of 2 A module has @register on 12 functions, and register prints the name of each function it receives. When are the 12 names printed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: When the module is first imported, as each def runs, before any of the functions is called

    The decorator runs once per function, when the def statement runs, so its side effects, such as printing or registering, happen while the module is being imported.

Methods and classes take decorators too

A method is a function defined in a class, so the same wrappers work on methods; self simply arrives as the first of *args. A class decorator: A decorator written above a class statement. It receives the class once the class body has run, and the class name is bound to what it returns, usually the same class, changed. receives the class instead of a function, after the class body has run, and returns a class, usually the same one, changed:

A method decorator and a class decorator Python · class_decorator.py
import functools


def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        shown = ", ".join(repr(a) for a in args[1:])   # args[0] is self for a method
        print(f"call {func.__qualname__}({shown})")
        return func(*args, **kwargs)
    return wrapper


def with_repr(cls):
    """A class decorator: give the class a __repr__ that lists its attributes."""
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({fields})"
    cls.__repr__ = __repr__
    return cls   # the same class, changed


@with_repr
class Ticket:
    def __init__(self, seat, fare):
        self.seat = seat
        self.fare = fare

    @log_calls
    def upgrade(self, extra):
        self.fare += extra
        return self.fare


ticket = Ticket("C12", 450)
print(ticket)
ticket.upgrade(150)
print(ticket)
print(Ticket.upgrade.__qualname__)   # the wrapper took the method's name from wraps

Output

Ticket(seat='C12', fare=450)
call Ticket.upgrade(150)
Ticket(seat='C12', fare=600)
Ticket.upgrade

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 class_decorator.py

with_repr adds a __repr__ method to Ticket and returns the class, so printing a ticket shows its attributes. log_calls wraps the method upgrade: self arrives as args[0], which the wrapper leaves out of the line it prints, and the last line shows that the wrapped method still reports its qualified name, Ticket.upgrade, because wraps copied it. Step through the order in which Python applies the two:

Interactive · stage by stage The order of events for a decorated class Step through the class statement of class_decorator.py, from its body to the name Ticket.
The order of events for a decorated class, step 5 of 5. Showing The class body runs, @log_calls wraps upgrade, The class object is made, with_repr(cls) runs, Ticket is bound. In focus: Ticket is bound.The class body runsit defines __init__ and upgrade@log_calls wraps upgradeas its def runs in the bodyThe class object is madenot bound to Ticket yetwith_repr(cls) runsit adds __repr__ to the classTicket is boundto what with_repr returned

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The body. Python runs the class body like a small module. def __init__ makes a function in the class's namespace.
  2. A method decorator. The def of upgrade is decorated, so log_calls(upgrade) runs right there, inside the body, and the wrapper takes the name upgrade.
  3. The class. When the body is done, Python makes the class object from its namespace. The name Ticket is not bound yet.
  4. The class decorator. with_repr receives the finished class, adds a __repr__ method to it and returns the same class.
  5. The name. Ticket is bound to what with_repr returned. Had it forgotten return cls, Ticket would be None.

The standard library is full of both kinds: @dataclass and @functools.total_ordering decorate classes, while @property, @staticmethod, @classmethod and @functools.cache decorate functions and methods. Later lessons cover each of them.

Checkpoint

Check: Decorating methods and classes

  1. One question A class decorator adds a __repr__ method to cls but forgets the line return cls. What is the class's name bound to afterwards, and what does the next Ticket(…) call do?

    Choose one answer.

    Show the answer: One question

    Answer: None, so creating an instance with Ticket(…) raises TypeError

    A class decorator works like any decorator: the name is bound to its return value. Without return cls that is None, and Ticket("C12", 450) fails with "'NoneType' object is not callable".

Practise: two guard decorators

The exercise asks for two decorators that guard a function. @validate_positive refuses negative numbers before the function runs, given by position or by keyword, and @call_limit(n) lets a function run at most n times, each function with a count of its own. Both must keep the decorated function’s name and docstring, so functools.wraps earns its place here:

Exercise · Medium · Python

Two guard decorators that keep the function's name

Write two decorators in guards.py:

  • @validate_positive checks the arguments of every call before the decorated function runs: if any argument that is an int or a float, given by position or by keyword, is negative, it raises ValueError and the function does not run. Zero is allowed, and arguments of other types, such as strings, are not checked.
  • @call_limit(n) lets the decorated function run at most n times. The next call raises RuntimeError without running the function. Every function decorated with call_limit has a count of its own.

Both decorators must keep the decorated function's __name__ and __doc__, and must return whatever the function returns.

Starter code · guards.py

import functools


def validate_positive(func):
    """Raise ValueError when an int or float argument is negative."""
    return func


def call_limit(n):
    """Let the decorated function run at most n times; later calls raise RuntimeError."""
    def decorate(func):
        return func
    return decorate
The sample tests · test_guards.py
from guards import call_limit, validate_positive


@validate_positive
def area(width, height):
    """Area of a rectangle."""
    return width * height


def raises(error, call):
    try:
        call()
    except error:
        return True
    return False


def test_non_negative_arguments_pass():
    """non-negative numbers, zero included, reach the function"""
    assert area(3, 4) == 12
    assert area(0, 5) == 0
    assert area(width=2.5, height=2) == 5.0


def test_negative_arguments_raise():
    """a negative number, by position or keyword, raises ValueError"""
    assert raises(ValueError, lambda: area(-1, 4))
    assert raises(ValueError, lambda: area(3, height=-0.5))


def test_other_types_are_not_checked():
    """strings and other types are not checked"""
    @validate_positive
    def repeat(text, times):
        return text * times

    assert repeat("ab", 2) == "abab"
    assert raises(ValueError, lambda: repeat("ab", -2))


def test_validate_keeps_name_and_doc():
    """@validate_positive keeps __name__ and __doc__"""
    assert area.__name__ == "area"
    assert area.__doc__ == "Area of a rectangle."


def test_call_limit():
    """the call after the n-th raises RuntimeError and keeps the metadata"""
    @call_limit(2)
    def ping():
        """Answer a ping."""
        return "pong"

    assert ping() == "pong"
    assert ping() == "pong"
    assert raises(RuntimeError, ping)
    assert ping.__name__ == "ping"
    assert ping.__doc__ == "Answer a ping."


def test_blocked_call_does_not_run():
    """a call over the limit never runs the function"""
    ran = []

    @call_limit(1)
    def record(value):
        ran.append(value)

    record("first")
    assert raises(RuntimeError, lambda: record("second"))
    assert ran == ["first"]


def test_each_function_has_its_own_count():
    """two limited functions count separately"""
    @call_limit(1)
    def first():
        return 1

    @call_limit(1)
    def second():
        return 2

    assert first() == 1
    assert second() == 2
A hint

validate_positive takes the function and returns a wrapper; inside the wrapper, (*args, *kwargs.values()) holds every argument of the call, and isinstance(value, (int, float)) picks the numbers. call_limit(n) is one level deeper: it returns the decorator, which returns the wrapper. Keep the count in the decorator's scope and declare it nonlocal in the wrapper. Put @functools.wraps(func) on each wrapper.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Stack your own decorators in a full editor and print __name__, __wrapped__ and inspect.signature() to see what each layer keeps.

To finish, a run of questions on wrapping, wraps, arguments, stacking and class decorators. It aims at the idea your answers so far show you know least, climbs a level when you answer well, and gives every choice its reason:

Practice that adapts to you

Practise this lesson

17 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 @cached written above def load(path): means the same as which line after the def?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: load = cached(load)

    Python creates the function, passes it to the decorator and binds the function's name to whatever the decorator returns.

  2. Question 2 of 17 What does this program print?

    Read the code, then choose one answer.

    def twice(func):
        def wrapper(x):
            return func(func(x))
        return wrapper
    
    def inc(x):
        return x + 1
    
    inc2 = twice(inc)
    print(inc(5), inc2(5))
    Show the answer: Question 2 of 17

    Answer: 6 7

    A decorator is just a function that takes a function. Applied by hand to a new name, as here, both the original and the wrapped version stay available: inc(5) is 6 and inc2(5) is 7.

  3. Question 3 of 17 Which decorator makes the decorated report() raise TypeError when it is called later?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: def broken(func): func.calls = 0

    The name is bound to whatever the decorator returns. A decorator that forgets to return anything leaves None behind, and the first call fails with "'NoneType' object is not callable".

  4. Question 4 of 17 What does this print?

    Read the code, then choose one answer.

    def logged(func):
        def wrapper(*args, **kwargs):
            print("calling", func.__name__)
            func(*args, **kwargs)
        return wrapper
    
    @logged
    def add(a, b):
        return a + b
    
    print(add(2, 3))
    Show the answer: Question 4 of 17

    Answer: calling add None

    The wrapper calls func but throws its result away, so the call returns None. The fix is return func(*args, **kwargs).

  5. Question 5 of 17 Why does a general wrapper take args and *kwargs?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: So that it accepts whatever arguments the original takes and passes them all on unchanged

    args collects the positional arguments into a tuple and *kwargs the keyword arguments into a dict; func(args, *kwargs) spreads them back out, so the original receives exactly what the caller passed.

  6. Question 6 of 17 The report in timed.py is in a finally block. What happens when the wrapped function raises an exception?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: The time is still printed, and then the exception goes on to the caller

    try/finally guarantees the report: the clock is read and the line printed whether func returns or raises, and an exception then continues to the caller as if the wrapper were not there.

  7. Question 7 of 17 A wrapper is decorated with @functools.wraps(func). Which of these does the decorated function now report from the original?

    Choose every answer that is right.

    Show the answer: Question 7 of 17

    Answer:

    • Its signature, through inspect.signature(), which follows __wrapped__
    • Its __name__ and __doc__
    • The original function itself, through __wrapped__

    wraps copies the name, qualified name, docstring and a few other attributes, and adds __wrapped__, which inspect.signature() follows. The wrapper still runs on every call, so the extra call costs the same.

  8. Question 8 of 17 What does @functools.wraps(func), placed on a wrapper, do?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: It copies the original's name, docstring and a few other attributes onto the wrapper and adds __wrapped__

    Without wraps, help(), repr() and logs that print the name show the wrapper's own name and no docstring. With it, the wrapper reports the original's identity, and __wrapped__ leads back to the original function.

  9. Question 9 of 17 In @retry(times=3), what does the call retry(times=3) return?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: A decorator, which will receive the function and return a wrapper

    With parentheses, Python first calls retry(times=3); its return value is the actual decorator, applied to the function below it. That is why decorators with arguments have three levels.

  10. Question 10 of 17 What does this program print?

    Read the code, then choose one answer.

    def retry(times=3):
        def decorate(func):
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper
        return decorate
    
    @retry
    def fetch(item):
        return item.upper()
    
    result = fetch("tea")
    print(callable(result), result.__name__)
    A hint

    Without parentheses, what does retry receive as times, and what does it return?

    Show the answer: Question 10 of 17

    Answer: True wrapper

    @retry without parentheses means fetch = retry(fetch), so retry runs with times set to the function and returns decorate. Calling fetch("tea") then calls decorate with "tea" as func, and that returns a wrapper instead of a result. Write @retry(), or @retry(times=3).

  11. Question 11 of 17 What does stacking.py print?

    What does this program print? Choose one answer.

    import functools
    
    
    def tag(label):
        def decorate(func):
            print(f"applying {label} to {func.__name__}")
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                print(f"  enter {label}")
                result = func(*args, **kwargs)
                print(f"  leave {label}")
                return result
            return wrapper
        return decorate
    
    
    @tag("outer")
    @tag("inner")   # applied first: it is closest to the def
    def work():
        print("    working")
        return 42
    
    
    print("--- now calling work()")
    print(work())
    Show the answer: Question 11 of 17

    Answer: it prints

    applying inner to work
    applying outer to work
    --- now calling work()
      enter outer
      enter inner
        working
      leave inner
      leave outer
    42

    Decorators run when the def runs, nearest first, so both "applying" lines come before the call, inner first. The name then refers to the outer wrapper, so a call enters outer, then inner, then the function, and leaves in the opposite order.

  12. Question 12 of 17 With @outer above @inner above a def, which decorator is applied first?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: inner, the one nearest the def

    Stacked decorators are applied from the bottom up: inner wraps the function, then outer wraps that result. A call then passes through them from the top down.

  13. Question 13 of 17 You want to time each attempt of a retried call separately, not all attempts together. Where does @timed go?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: Below @retry(times=3), nearest the def

    The wrapper nearest the def runs closest to the original. Put @timed below @retry to measure each attempt, and above it to measure the whole retried call.

  14. Question 14 of 17 When does the COMMANDS dictionary in registry.py get its two entries?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: As each decorated def runs, before any command is called

    command("add") returns register, and register(add) runs at the def, storing the function. The dictionary is full after the defs and before any call, as the line "after the defs" shows.

  15. Question 15 of 17 A decorator opens a database connection when it is applied, to check a table. What is the consequence?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: Importing the module connects to the database, and fails if it is down, even when no decorated function is ever called

    Work done in the decorator itself happens at definition time, so a slow or failing decorator slows or breaks the import of the whole module. Move such work into the wrapper, or into a function called when it is needed.

  16. Question 16 of 17 What does a class decorator receive, and what must it return?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: The class, after its body has run; it returns a class, usually the same one, changed

    A class decorator is called with the class object once the class body has run, and the class name is bound to what it returns: with_repr adds __repr__ and returns the same class.

  17. Question 17 of 17 In class_decorator.py, why does the wrapper of log_calls print args[1:] and not all of args?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: Because args[0] is self, the instance the method was called on

    The same wrapper works on methods because self simply arrives as the first of *args. log_calls leaves it out of the line it prints, which shows call Ticket.upgrade(150).

Interview questions

Fresher to 2–5 years · What does functools.wraps copy?

functools.wraps(func) is a decorator for the wrapper that calls functools.update_wrapper(). It copies the attributes named in functools.WRAPPER_ASSIGNMENTS from the original function to the wrapper: on Python 3.14, __module__, __name__, __qualname__, __doc__, __annotate__ and __type_params__. It updates the wrapper’s __dict__ with the original’s, so custom attributes survive, and it adds __wrapped__, a reference to the original function. Attributes the original lacks are skipped without an error. It does not change what the wrapper accepts, which is still *args and **kwargs, but inspect.signature() follows __wrapped__ and reports the original signature. Without it, help(), repr() and logs that print the function’s name show wrapper and lose the docstring.

Key takeaways

  • @decorator above a def means func = decorator(func): the name refers to whatever the decorator returns.
  • A wrapper takes *args and **kwargs, passes them on and returns the original’s result; without that return, every call gives None.
  • Put @functools.wraps(func) on every wrapper: it keeps the name, docstring and qualified name and adds __wrapped__, which inspect.signature() follows.
  • A decorator with arguments is a function that returns a decorator: three levels, and the parentheses matter.
  • Stacked decorators are applied from the bottom up, and a call passes through them from the top down.
  • Decorators run when the function or class is defined, so their side effects happen at import time, and a class decorator must return the class.

Terms in this lesson

class decorator
A decorator written above a class statement. It receives the class once the class body has run, and the class name is bound to what it returns, usually the same class, changed.
decorator
A function that takes a function and returns the one its name should refer to, usually a wrapper. @timed above def report(): has the same effect as report = timed(report) after it.
decorator with arguments
A function that takes settings and returns a decorator, such as retry(times=3). Python calls it first, at the def, and applies the decorator it returns to the function.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.