decorators
Decorators deep dive
Prerequisite concept: "top level"
“Top-level” doesn’t mean it’s near the top of the file.
It means: “outside of any other block,” and executed immediately when the module is loaded.
It doesn’t mean “at the beginning of the file.” It means outside of any function/class definitions.
🧪 Example: What's Top-Level vs Not
print("✅ Top-level 1") # ← top-level
def foo():
print("❌ NOT top-level - inside function")
print("✅ Top-level 2") # ← still top-level
class MyClass:
print("❌ NOT top-level - inside class body")
if __name__ == "__main__":
print("✅ Still top-level - in the global scope")
🔎 Why It Matters
Top-level (runs at module load):
├── Imports
├── Variable assignments
├── Function/class definitions
├── print() calls
Not top-level (runs later):
└── Inside def, class, if, for, while, try...
Top-level code is executed immediately when:
- the module is imported
- or run as a script
Code inside functions, classes, conditionals, etc. isn’t executed until the interpreter enters those blocks during execution.
What does "definitions are executed, but not their body" really means
This is one of those deceptively deep Python ideas that shows how "code is data" in Python. When you write a function or class in Python:
def foo():
print("Hello")
class Bar:
print("Inside class")
def method(self):
print("Inside method")
Python executes the def or class statement during module loading — but not the code inside the function or method body.
Function/Class Definitions Are Statements
def foo(): ...
class Bar: ...
These lines are executed like regular statements — they:
- Create a function object or class object
- Bind that object to a name in the current scope
✅ That’s what we mean when we say the definition is executed.
Function or Method Bodies Are Not Run Immediately
The body of the function (i.e., the code inside the def) is:
- not executed right now
- just stored inside a code object in the function/class
It only runs when the function or method is called later.
print("💡 Module loading")
def foo():
print("🚀 Inside foo")
print("✅ foo is defined")
foo() # <- now the body runs
💡 Module loading
✅ foo is defined
🚀 Inside foo
Notice:
def foo():got executed — afunctionobject was created and bound to the namefoo- The
print("🚀 Inside foo")did not run untilfoo()was called
🎯 For Classes, It's Slightly Different
When Python hits a class statement:
class MyClass:
print("🔧 Inside class body")
def method(self):
print("🚀 Inside method")
- The class body is run immediately, just like module-level code
- The
method()function object is created and stored in the class’s namespace - But the method body is not executed until called
class MyClass:
print("🔧 Class body running")
def method(self):
print("🚀 Method body running")
print("✅ Class defined")
Output:
🔧 Class body running
✅ Class defined
Calling MyClass().method() would then trigger:
🚀 Method body running
| Concept | Happens at Definition | Happens at Call |
|---|---|---|
def foo(): ... |
✅ Function is created | ❌ Body not run |
foo() |
❌ Definition ignored | ✅ Body runs |
class Bar: ... |
✅ Class body runs | ❌ Methods not run |
Bar().method() |
❌ Class already defined | ✅ Method body runs |
Prerequisite concept: 🧭 Timeline in Python Execution
When you run or import a Python module, here’s what happens in order:
Start
├── Module Loading Time
│ ├── Top-level code execution
│ ├── Function/class definitions (definition time)
│ │ └── Decorators take effect here
│ └── All code outside functions/classes is executed
└── Runtime
└── Functions and classes get called or used
📦 Module Loading Time
This happens:
- When a script is executed directly (
python my_script.py) - Or imported as a module
What happens:
- Python parses and executes the top-level code line by line.
- This includes defining functions, classes, variables, and applying decorators.
At this point, function bodies aren't executed yet — only the definitions are registered in memory.
🏗️ Definition Time
This happens during module loading for each function/class definition.
What happens:
- The
defstatement creates a function object. - Then, decorator syntax (the
@decorator) is applied immediately.
@my_decorator
def foo():
...
This is really:
def foo():
...
foo = my_decorator(foo)
So the
@sugar runs right after the function object is created. This is definition time, not runtime.
🚀 Runtime
This is when the user (or program) calls the function or class.
What happens:
- The function/class is already decorated.
- Whatever the decorator returned (often a wrapped function) is now what's being called.
📌 When Do Decorators "Take Effect"?
Decorators take effect at definition time, which happens during module loading.
🎯 Summary Table
| Phase | What Happens | When @decorator applies |
|---|---|---|
| Parsing | Python parses your .py file (doesn't run anything) |
❌ |
| Module Load | Top-level code is executed | ✅ |
| Definition Time | def foo() creates a function object |
✅ |
| Decorator Time | foo = my_decorator(foo) |
✅ |
| Runtime | You call foo() (already decorated!) |
❌ |
🧠 Are There Other "Times"?
Some other relevant phases:
- Import time: When a module is imported, it's essentially the same as module loading time.
- Compilation time: Python compiles
.pyto.pyc(bytecode) – decorators don’t run here. - Initialization time: Sometimes refers to when classes or modules initialize variables – still under "module loading."
So the core phases for decorators are really:
- Module loading
- Definition time (inside module loading)
Elaboration with code
print("📦 Module loading starts")
def simple_decorator(func):
print("🔧 Decorator is being applied to:", func.__name__)
def wrapper(*args, **kwargs):
print("🚀 Wrapped function is called!")
return func(*args, **kwargs)
return wrapper
@simple_decorator
def greet():
print("👋 Hello, world!")
print("📦 Module loading ends")
print("⏱ Now we call greet() at runtime:")
greet()
📦 Module loading starts
🔧 Decorator is being applied to: greet
📦 Module loading ends
⏱ Now we call greet() at runtime:
🚀 Wrapped function is called!
👋 Hello, world!
🧪 Illustrative Separation
print("📦 Module loading starts") # Module loading
def foo():
print("⏱ This is runtime — inside foo") # Runtime
print("📦 Module loading ends") # Still module loading
print("⏱ Now we call foo():") # Still module loading
foo() # Triggers runtime
Technically all print() calls are executed at runtime, because Python is an interpreted language — there's no separate compile phase like in C++.
But when we talk about module loading time vs runtime, we're making a conceptual distinction in when code is executed relative to function calls.
✅ Module Loading Time
- When the Python interpreter executes top-level code in a module.
- This includes:
- Imports
- Function and class definitions
- Decorator application
- Top-level print() calls
→ This happens once, when the module is imported or run directly.
print("📦 Module loading starts") # <-- Executed immediately as the script starts
✅ Runtime (Function Execution Time)
- Happens after the module has been loaded.
- Involves code inside functions or methods — i.e., the stuff that only runs when the function is called.
def greet():
print("👋 Hello!") # <-- This runs only when greet() is called
| Code location | When it runs | What it's called |
|---|---|---|
Top-level print() |
When the module is loaded | Module loading time |
@decorator |
When function is defined | Definition time (during loading) |
Code inside def |
When the function is called | Function runtime |
🔍 What Exactly Happens at Definition Time?
🧱 1. @simple_decorator is syntax sugar
This:
@simple_decorator
def greet():
...
Is equivalent to:
def greet():
...
greet = simple_decorator(greet)
So Python:
- Defines
greetas a function object (like normal). - Immediately calls
simple_decorator(greet). - Assigns the result of that call (which will be
wrapper) back togreet.
🧩 Inside simple_decorator(func) — Definition Time
Here’s what happens step-by-step:
print("🔧 Decorator is being applied to:", func.__name__) # ✅ Executed
✅ This line runs immediately when the decorator is applied.
That’s why you see the print when the module loads.
def wrapper(*args, **kwargs): # ✅ This is executed (definition of function object)
print("🚀 Wrapped function is called!")
return func(*args, **kwargs)
✅ The wrapper function is defined (i.e., Python creates a function object named wrapper), but its body is not executed yet. It’s just created as a function object in memory.
return wrapper # ✅ Executed — returns the function object
✅ This line is executed immediately. It returns the wrapper function object to the caller.
So now:
greet = wrapper
🚨 Important Clarification
greet = wrapper(greet())← ❌ Not what's happening!
What’s really happening is:
greet = simple_decorator(greet) # greet is the undecorated function object
# Inside the decorator:
return wrapper # ← returns a function, NOT the result of calling it
E.g. greet becomes a reference to a new function, wrapper, which closes over the original greet.
def simple_decorator(func): # func == original greet function
print("🔧 Decorator is being applied to:", func.__name__)
def wrapper(*args, **kwargs): # ← This function closes over `func`
print("🚀 Wrapped function is called!")
return func(*args, **kwargs) # ← uses the original function!
return wrapper # ← returns the new function
So at @simple_decorator, Python does:
- Define
greet()(the original function) - Pass it into
simple_decorator→func = greet - Define
wrapper(...), which remembersfuncvia closure - Return
wrapper - Assign:
greet = wrapper
🔐 Closure Magic
The function wrapper(...) remembers the func that was passed into simple_decorator.
That’s how wrapper knows what to call later:
return func(*args, **kwargs) # this func is the original greet
✅ func is bound to the original undecorated greet()
✅ This binding happens once during decorator execution
✅ Summary with Objects
Think of it this way:
| Name | Points to | Notes |
|---|---|---|
greet |
wrapper function |
after decorator application |
wrapper |
uses func in closure |
func == original greet() |
So when you do:
greet()
You're really doing:
wrapper() # calls original greet() from the closure
✅ Closure in Python vs Shared Pointers in C++
🌐 Python Closure: What It Is
In Python, a closure happens when:
- A nested function (
wrapper) references a variable from an enclosing scope (func) - That enclosing scope is no longer active, but its referenced variables stay alive because they’re captured.
So when simple_decorator() returns, func (original greet) would normally go out of scope…
BUT since wrapper still closes over it, Python keeps func alive.
🤝 Analogy: Shared Pointer in C++
In C++, you could imagine something like:
std::shared_ptr<void()> greet = std::make_shared<...>();
std::function<void()> wrapper = [greet]() {
(*greet)(); // call the original greet
};
This maps very well to the Python model:
greetis captured by value (shared_ptr copy) into the lambda- Even if the outer
greetgoes out of scope, the lambda (wrapper) holds a reference greetis only destroyed when no one holds a ref → just like in Python!
So we can say this is accurate:
“func will be destructed (ref count -1), but wrapper's body still holding the ref count, hence it lives longer”
🧠 How Python Does It Under the Hood
Python’s version is slightly lower-level than shared_ptr but similar in spirit:
- The variables
wrapperuses from the enclosing scope (likefunc) are stored in__closure__ - These are actually cell objects (similar to a
shared_ptr<T>— just not named that way) - Python’s garbage collector keeps track of these cell references
You can inspect them:
greet.__closure__[0].cell_contents
✅ TL;DR: Summary Table
| Concept | Python | C++ Analogy |
|---|---|---|
| Closure | Nested function holds enclosing var | Lambda captures by shared_ptr |
| Lifetime extension | Via __closure__ cell references |
Via shared_ptr ref count |
| Execution context | Enclosing scope gone, but values stay | Stack frame gone, but heap shared |
| Memory mgmt | Reference counting (plus GC) | Reference counting (shared_ptr) |
So yes: treat closures like shared_ptr to the captured values, and it’s a fantastic mental model — especially coming from C++.