---
title: Decorators
source: 'https://academia.sh/en/courses/python-data-structures/decorators'
course: 'Data Structures and Functional Tools'
language: en
updated: '2026-08-17T18:10:26+00:00'
license: 'CC BY-SA 4.0'
---

# Decorators

Wrapping does not delete the original function, it adds a layer on top: one decorator builds two layers, two decorators build three, and when metadata is not preserved, the function's name, doc, and signature change to the wrapper's.

In the previous lesson functions always passed as arguments, and none of them was
changed. But the same lesson also measured that a function can be **returned**:
`factor_maker` produced a new function object on every call. The two directions can
be combined — a function that **takes** a function and **returns** another function
**in its place**.

Such a function is called a **decorator**, and the object it returns is called a
**wrapper**. This lesson's questions are: how many objects does wrapping bring into
existence, what happens to the original function, and what does the wrapped
function's `__name__` attribute say? The previous lesson's seemingly minor
column — the name gap between `def` and `lambda` — will produce a result here.

## The Wrapper Pattern

The pattern has three steps. A decorator takes a function; defines a new function in
its body; returns that new function. When the inner function is called, it does its
own work and **also calls the function it received** — this is how the original
behavior is preserved while something is added around it.

The form written with `@` before a name is a shorthand. When a `@decorator` line
sits above a function definition, once the definition finishes,
`compute = decorator(compute)` runs. So nothing new is set up underneath it — the
previous lesson's higher-order function has its result bound back to the same name.

An important consequence follows: **the original function is not destroyed.** The
name is now bound to the wrapper, but the wrapper's body refers to the original
object and keeps it alive. Wrapping is not a deletion, it is **adding a layer** —
and layers can be counted.

## Two Separate Moments

Wrapping does work at two separate moments, and the two are often confused.

**Definition time:** the decorator's body runs and produces the wrapper. This
happens once — where the function is defined, even if it is never called.

**Call time:** the wrapper's body runs. This happens on every call.

The measurable consequence of this split is this: the work inside the decorator body
runs as many times as functions are wrapped; the work inside the wrapper body runs
as many times as calls are made.

The measurement's assumptions:

- **IF45** — The shared reference's `Item` class is used exactly as is; the
  decorator body, the wrapper, and the original body are kept in separate counters.
  The oracle is the rig itself.
- **IF46** — Layer count is found by following the `__wrapped__` chain; this chain
  is only built when metadata preservation is used.
- **IF47** — All four forms wrap the **same** original function, and all four are
  called once. Counters are reset before the measurement.
- **IF48** — The parameterized decorator deliberately calls the original body
  **three** times; the number is arbitrary and chosen only to make the wrapper's
  added behavior visible.
- **IF49** — The metadata measurement compares three forms: unwrapped, one that does
  not use preservation, and one that does. **Identity is shown with `is`, no number
  is printed.**
- **IF50** — The doc column asks whether the doc exists, not what it contains; the
  signature column is taken from the standard library's introspection tool.
- **IF51** — In the memoization measurement, twenty requests collapse into five
  distinct values; the request sequence is fixed, and the ratio of repeated requests
  is the measurement itself.
- **IF52** — In the order measurement, the two decorators are the **same**
  decorator and differ only by their labels; the trace records entry and exit points
  separately so order can be read.
- **IF53** — The narrow-signature wrapper deliberately accepts only a single
  positional argument; what is measured is that the call fails, and which exception
  class is raised.

## Measurement

```python
"""Decorator: how many layers does wrapping build, and what is lost when metadata is not preserved."""

import functools
import inspect

COUNTER = {"produced": 0, "decorator": 0, "wrapper": 0, "body": 0}


class Item:
    """An item that counts every production of itself."""

    def __init__(self, value):
        COUNTER["produced"] += 1
        self.value = value

    def __repr__(self):
        return f"Item({self.value})"


def reset():
    for k in COUNTER:
        COUNTER[k] = 0


def bare(function):
    """Wrapper that does not preserve metadata."""
    COUNTER["decorator"] += 1

    def wrapper(*a, **k):
        COUNTER["wrapper"] += 1
        return function(*a, **k)
    return wrapper


def guarded(function):
    """Wrapper carrying metadata via functools.wraps."""
    COUNTER["decorator"] += 1

    @functools.wraps(function)
    def wrapper(*a, **k):
        COUNTER["wrapper"] += 1
        return function(*a, **k)
    return wrapper


def repeating(times):
    """Parameterized decorator: the factory is called first, then the decorator."""
    def decorator(function):
        COUNTER["decorator"] += 1

        @functools.wraps(function)
        def wrapper(*a, **k):
            COUNTER["wrapper"] += 1
            for _ in range(times):
                result = function(*a, **k)
            return result
        return wrapper
    return decorator


def compute(n: int):
    """Produces an item from the given number."""
    COUNTER["body"] += 1
    return Item(n)


def layer_count(function):
    count = 1
    while hasattr(function, "__wrapped__"):
        function, count = function.__wrapped__, count + 1
    return count


def core(function):
    while hasattr(function, "__wrapped__"):
        function = function.__wrapped__
    return function


reset()
unwrapped = compute
single = guarded(compute)
double = guarded(guarded(compute))
parameterized = repeating(3)(compute)
at_definition = COUNTER["decorator"]

print(f"{'form':<26s} {'layers':>7s} {'core is original':>18s}"
      f" {'body calls':>12s}")
for name, f in (("unwrapped", unwrapped), ("single decorator", single),
                ("two decorators", double), ("parameterized decorator", parameterized)):
    COUNTER["body"] = 0
    f(7)
    print(f"  {name:<24s} {layer_count(f):7d}"
          f" {str(core(f) is compute):>18s} {COUNTER['body']:12d}")

print()
print(f"decorator body run while building the four forms: {at_definition};"
      f" wrapper run once each is called:"
      f" {COUNTER['wrapper']}")
print(f"single is compute -> {single is compute},"
      f" single.__wrapped__ is compute -> {single.__wrapped__ is compute}")

print()
print(f"{'form':<16s} {'__name__':>12s} {'has doc':>9s} {'signature':>18s}")
for name, f in (("unwrapped", compute), ("no wraps", bare(compute)),
                ("with wraps", guarded(compute))):
    print(f"  {name:<14s} {f.__name__:>12s} {str(bool(f.__doc__)):>9s}"
          f" {str(inspect.signature(f)):>18s}")


@functools.cache
def expensive(n):
    COUNTER["body"] += 1
    return Item(n % 5)


reset()
requests = [i % 5 for i in range(20)]
for i in requests:
    expensive(i)
print()
print(f"memoization: {len(requests)} requests, body calls {COUNTER['body']},"
      f" produced {COUNTER['produced']}, held in cache"
      f" {expensive.cache_info().currsize}")

TRACE = []


def tracing(label):
    def decorator(function):
        @functools.wraps(function)
        def wrapper(*a, **k):
            TRACE.append(f"{label} enter")
            result = function(*a, **k)
            TRACE.append(f"{label} exit")
            return result
        return wrapper
    return decorator


@tracing("outer")
@tracing("inner")
def top_down(n):
    TRACE.append("body")
    return Item(n)


@tracing("inner")
@tracing("outer")
def bottom_up(n):
    TRACE.append("body")
    return Item(n)


print()
for name, f in (("@outer @inner", top_down), ("@inner @outer", bottom_up)):
    TRACE.clear()
    f(1)
    print(f"  {name:<14s} {' -> '.join(TRACE)}")


def narrow(function):
    @functools.wraps(function)
    def wrapper(n):
        return function(n)
    return wrapper


@narrow
def two_args(n, k=2):
    return n * k


try:
    two_args(3, k=5)
except TypeError as e:
    print(f"narrow-signature wrapper with a keyword argument: {type(e).__name__}")
print(f"wide-signature wrapper on the same call:"
      f" {tracing('x')(lambda n, k=2: n * k)(3, k=5)}")
```

```
form                        layers   core is original   body calls
  unwrapped                      1               True            1
  single decorator               2               True            1
  two decorators                 3               True            1
  parameterized decorator        2               True            3

decorator body run while building the four forms: 4; wrapper run once each is called: 4
single is compute -> False, single.__wrapped__ is compute -> True

form                 __name__   has doc          signature
  unwrapped           compute      True           (n: int)
  no wraps            wrapper     False          (*a, **k)
  with wraps          compute      True           (n: int)

memoization: 20 requests, body calls 5, produced 5, held in cache 5

  @outer @inner  outer enter -> inner enter -> body -> inner exit -> outer exit
  @inner @outer  inner enter -> outer enter -> body -> outer exit -> inner exit
narrow-signature wrapper with a keyword argument: TypeError
wide-signature wrapper on the same call: 15
```

## The Number of Layers

In the top table, the layers column counts directly: the unwrapped function is
**1**, a single decorator is **2**, two decorators are **3**. Every wrapping adds
one link to the chain, and link count is one more than decorator count.

The second column says the more important thing: on every row, the object sitting
at the bottom of the chain is **the original function itself**. Wrapping neither
changes nor destroys the original object; it keeps it alive at the end of a
reference. The bottom two lines show this directly: `single is compute` is
**false** — the name is now bound to a different object — but
`single.__wrapped__ is compute` is **true**. A new object was materialized, the old
object was **shared**.

Read from the course's axis, wrapping is not a copy, it is **building a container**:
the outer object is new, the one inside it is old. The same pattern as the first
topic's slicing measurement, this time on functions.

The fourth row shows that a layer adds **behavior**: when the function wrapped by
the parameterized decorator is called once, the original body runs **3** times.
Layer count is still **2** — the parameter does not add a layer, it only decides
the layer's behavior. The parameterized decorator has three stages: first the
factory is called and produces the decorator, then the decorator is called and
produces the wrapper, then the wrapper is called.

The number's cost side reads just as directly. Every wrapping materializes a
function object and keeps the original object alive; a function wrapped by two
decorators leaves **3** function objects standing in memory, and every call opens
three frames. Wrapping a function is cheap, but not free, and the cost is paid **per
layer**.

The next line separates the two moments. While the four forms were being built, the
decorator body ran **4** times — once for every wrapped function, before any of them
was called. The wrappers, in turn, ran only once calls were made. The work at
definition time is paid once; the work at call time is paid on every call.

## Lost Metadata

The middle table answers the lesson's second question, and the middle row shows the
loss.

When metadata preservation is not used, the wrapped function's `__name__` attribute
becomes **wrapper**, its doc **disappears**, and its signature shows as
`(*a, **k)`. All three pieces of information still sit on the original object, but
nobody looks at it anymore: the object bound to the name is the wrapper, and these
three attributes are its own.

Where the loss is paid matters. The name appears in error traces and log records; if
ten separate functions are wrapped by the same decorator, all ten carry the same
name, and a trace cannot say which function ran. The doc is read in online help and
in doc generation. The signature is the most deceptive of the three: `(*a, **k)` is
not a signature, it is the absence of one — no tool inspecting which arguments are
expected gets a correct answer.

The bottom row shows the fix. The standard library's `wraps` tool is a decorator,
and when applied to a wrapper, it copies the original object's name, doc, and
signature information onto it, and also sets up the `__wrapped__` link. The layer
count in the top table was possible exactly because of this link — without
preservation the chain cannot be followed, and what lies beneath a wrapped function
cannot be seen from outside.

The distinction in one sentence: the wrapper preserves the original **behavior** on
its own, because it calls it; it does not preserve the original **identity**,
because that has to be carried separately.

## What Retention Buys

The last line measures one of the decorator's most common uses, and it reverses the
course's axis. Twenty requests are made, and they collapse into five distinct
values; the original body runs only **5** times, produced is **5**, and held in
cache is **5**.

Unwrapped, twenty requests would have meant twenty body calls and twenty objects.
The wrapper cuts production to a quarter by **storing** the result for the arguments
it has seen. The Programming Fundamentals course established memoization as a
technique; the contribution here is giving it a number — and the number has to be
read across two columns at once.

Production dropped from **20** to **5**, but retention rose from **0** to **5**.
The third lesson measured that laziness lowers retention; this line does the
opposite: it raises retention to lower production. The two measurements together
give the course's axis its final form — **there is a trade-off between
materializing and retaining, and which side to land on depends on the job itself.**
For a thousand items used once and discarded, not retaining is correct; for five
values that will be asked for twenty times, retaining is correct.

The cache's unbounded growth is this trade-off's open end: retained count grows with
the number of distinct arguments, and nothing shrinks it. The standard library
therefore also offers a bounded form; once a bound is set, retention is capped and
body calls rise again.

## Order and Signature

The last four lines measure two practical details of wrapping.

When two decorators are stacked, **order changes the result.** With `@outer` on
top, the trace runs: outer enter, inner enter, body, inner exit, outer exit. When
the order is reversed, the trace reverses too. The rule is: **the bottommost
decorator applies first** and lands deepest in the chain; the topmost applies last
and sits outermost. Call order is the reverse of this — the outermost layer runs
first, the body is reached last, and on the way back the layers close in reverse
order.

The distinction is not minor. If one layer does access control and another does
logging, which one is outermost decides whether a rejected call gets logged at all.
The two forms look the same; the trace differs.

The bottom two lines show why a wrapper's signature is written wide. A wrapper
accepting only a single positional argument cannot pass a call through even if the
original function accepts a keyword argument, and a `TypeError` is raised. The same
call works on a wide-signature wrapper that passes arguments through as given, and
gives **15**.

The reason is that the wrapper is now the **call surface**: every argument form
coming from outside passes through it first. If it does not accept every form the
original function accepts, it cannot pass through the ones it does not accept. The
variable-argument collecting form is therefore not a style preference but a
requirement of wrapping — and the `(*a, **k)` signature in the earlier table is
exactly the trace of this. When preservation is not used, the signature visible from
outside is this wide form; when it is used, the visible signature is the original
function's, but what actually satisfies the call is still the wide form.

## Summary

- A decorator takes a function and returns another function in its place; the `@`
  form is shorthand for the binding `compute = decorator(compute)`.
- Wrapping does not destroy the original object, it adds a layer: unwrapped **1**,
  single decorator **2**, two decorators **3** layers. The object bound to the name
  changes (`single is compute` is false) but the original object is shared
  (`single.__wrapped__ is compute` is true).
- The decorator body runs **at definition time**, once per wrapped function; the
  wrapper runs **at call time**, on every call. The parameterized decorator has
  three stages and does not increase layer count.
- When metadata is not preserved, the name becomes `wrapper`, the doc is empty, and
  the signature is `(*a, **k)`; when preservation is used, all three take on the
  original value and the `__wrapped__` link is set up. The wrapper preserves
  behavior on its own; identity has to be carried separately.
- A memoizing wrapper cuts twenty requests down to **5** body calls and retains
  **5** items in exchange: production drops, retention rises. The direction of the
  trade-off depends on the job itself.
- The bottommost decorator applies first and sits innermost; call order is the
  reverse. Because the wrapper is a call surface, it has to pass arguments through
  as given — a narrow-signature wrapper raises a `TypeError` on a call the original
  function accepts.

## Next Step

Up to this point, everything sat inside **a single file**. Every measured class,
every generator, every decorator was defined in the same body and used in the same
body; the question of where a name is written never came up, because there was only
one place to write it. Yet this lesson's wrapper could just as well have wrapped a
function defined in another file. When code is spread across multiple files, where
are names written, how does one file see another's names, and if the same file is
requested twice, is it read twice? The next topic sets up this machinery.
