At what point does a Python decorator's own body run relative to calls?
answer
- Two clocks, not one
- The def statement is the trigger
- Once per module per process
- sys.modules stops a second execution
- Per-call work belongs in the wrapper
basics
~20 sThe decorator body runs once, when the def it sits above is executed — normally while the module is first imported. Only the wrapper it returns runs per call, so anything done in the decorator body is import-time work, not per-call work.
solid answer
~40 sDecoration happens at definition time. Executing a module runs its top-level statements in source order, and each `def` builds the function object and immediately calls its decorator, so the decorator body has already run before anything calls the function. Because `sys.modules` caches an executed module, that happens once per process, not once per `import` statement. Everything created in the decorator body — a counter, a lock, a config lookup, a captured timestamp — is created once and shared by every call through the closure; anything that must be per-call belongs inside the wrapper. The practical consequences are all timing: import-time side effects fire before `main()` has configured logging or read the environment, and their order follows import order, which the definition site does not control.
code
pycon · 13 lines>>> def register(func):
... print("decorating", func.__name__)
... return func
...
>>> @register
... def pick_list():
... return []
...
decorating pick_list
>>> pick_list()
[]
>>> pick_list()
[]go deeper
Recall the split: the decorator runs once when the def is executed, the wrapper runs on every call. Being able to predict which print statement fires when is enough at this level.
Explain that module bodies execute on first import and are cached in sys.modules, so decoration is once per process, and that state created in the decorator body is shared by all calls.
Show the diagnosis: values frozen at import, work paid by every process that imports the module, logging emitted before configuration, and cross-module ordering decided by the import graph.
Own the policy — how much behaviour your codebase is willing to attach at import time, given that it runs before start-up, cannot be turned off per environment, and fails as an import error.
### Two clocks, not one A decorator involves two moments that beginners collapse into one. **Decoration time** is when the decorator function itself runs: the `def` statement executes, builds a function object, hands it to the decorator, and binds the name to the return value. **Call time** is when the returned wrapper runs. The decorator body executes exactly once per `def`; the wrapper executes once per call. Knowing which of the two a line of code is in explains most surprising decorator behaviour. ### When decoration time actually is A `def` at module level is a top-level statement, and top-level statements run when the module is executed — which for an imported module means the first time it is imported anywhere in the process. Import machinery then records the module in `sys.modules`, so subsequent `import` statements for the same name rebind a reference and do **not** re-execute the body. Decoration therefore happens once per module per process, no matter how many modules import it. (`importlib.reload` deliberately re-executes the module and so re-runs the decoration, which is exactly why reloading a module with a registration decorator can produce duplicate entries.) Within a single module the order is plain source order, top to bottom. Across modules it is import order, which is decided by the import graph and by whatever your entry point touches first — not by anything visible at the definition site. Any design that depends on decorators running in a particular sequence across files has an ordering assumption that nobody can see or verify locally. A `def` nested inside another function is different: its decorator runs each time the outer function runs, because the inner `def` is an ordinary statement in the outer body. That is a real cost if the decorator is expensive, and a real bug source if the decorator registers something, since each outer call registers again. ### The scenario that teaches it A nightly warehouse pick-list builder runs for about six hours. Someone writes a decorator that records the run date and stamps every pick list with it. Written in the decorator body — ```python import datetime def stamped(func): day = datetime.date.today() # decoration time: once def wrapper(*args, **kwargs): return day, func(*args, **kwargs) return wrapper ``` — `day` is evaluated once, when the module was imported. A job that starts at 22:00 and finishes at 04:00 stamps every list produced after midnight with the previous day, and the bug is invisible in a short test run. Move `datetime.date.today()` inside the wrapper and each call re-evaluates it. The general rule: per-call values belong in the wrapper; only genuinely shared state — a cache dict, a counter, a lock, a compiled pattern — belongs in the decorator body, where being created once is the point. The same run shows the ordering hazard. If two modules each decorate steps at import, the sequence in which those decorator bodies ran is the sequence in which their modules happened to be imported. Rearranging an import, or adding one, silently reorders them. ### Import-time side effects Because decorator bodies execute during import, anything they do happens before your program's own start-up has finished. That has consequences worth stating explicitly: - **Logging is not configured yet.** A decorator that logs at decoration time emits before `main()` calls the logging configuration, so the message is formatted by default handlers or dropped entirely. - **Configuration and the environment may not be loaded.** Reading a setting at decoration time freezes the value that existed at import, so later changes are ignored, and any code path that sets the environment after import has no effect. - **The cost is paid by every process that merely imports the module** — including a `--help` invocation, a test collection run that imports the module without calling anything, a documentation build, and every worker process a server forks or spawns. - **Failures at decoration time are import failures.** An exception in a decorator body surfaces as an `ImportError`-shaped traceback from the import statement, not from any call, which makes it a confusing place to do fallible work such as opening a file or a socket. Keep the decorator body cheap, deterministic and side-effect-free where you can; put fallible or configuration-dependent work in the wrapper, guarded so it happens on first call. ### Consequences for control `@` is unconditional: the decoration happens when the module executes, so you cannot express "decorate only in production" with the `@` line alone. The two workable shapes are to decide inside the wrapper at call time — checking a flag that later configuration can change — or to skip the `@` and rebind the name by hand under a condition. And because decoration is once-per-process, a decorated function cannot be undecorated at runtime except by rebinding the name to something else.
- A module is imported by five other modules. How many times does its decorator body run?Once. The first import executes the module body and records it in `sys.modules`; every later import of the same name binds a reference to the cached module object without re-executing it. The exception is a deliberate `importlib.reload`, which re-executes the body and therefore re-runs every decoration in it.
- What changes when the decorated def is nested inside another function?The inner `def` is an ordinary statement in the outer function's body, so it executes — and the decorator runs — on every call to the outer function. A fresh function object and a fresh wrapper are created each time, which is wasteful if the decorator is expensive and duplicates work if it registers or caches anything.
- How would you make a decorator's effect conditional on runtime configuration?Not with the `@` line, which is unconditional at import. Either check the flag inside the wrapper on each call, so later configuration is respected at the cost of a per-call test, or omit the `@` and rebind the name by hand under the condition, accepting that the decision is frozen at import time.
Fitting a new lock to a door is a one-off job done when the door is hung; turning the key is what happens on every visit. Work done during installation cannot see who eventually walks through.
saying these in an interview costs you the question
- Thinks the decorator runs on every call
- Believes each import statement re-runs decoration
- Puts per-call values in the decorator body
- Does fallible I/O at decoration time
- Assumes cross-module decoration order is stable
- Expects @ to be skippable at runtime