🧠 Python DeepCuts — 💡 Under the Hood of Decorators
Posted on: November 19, 2025
Description:
Decorators are one of Python’s most powerful features — yet also one of the most misunderstood.
They let you modify or extend function behavior without changing the function’s code, making them essential in:
- Logging
- Authentication
- Caching
- Validation
- Framework internals (Flask, FastAPI, Django, Typer, Click, etc.)
But how do decorators actually work under the hood?
This DeepCut explores:
- How decorators use closures
- How @decorator is expanded by Python
- Why functools.wraps is essential
- How decorators stack and wrap each other
Let’s decode the magic.
🧩 What Is a Decorator, Really?
A decorator is just a function that takes a function and returns a function.
The returned function (often called wrapper) replaces the original.
def simple_decorator(func):
def wrapper():
print("Before call")
func()
print("After call")
return wrapper
def greet():
print("Hello!")
decorated = simple_decorator(greet)
decorated()
Output:
Before call
Hello!
After call
The decorator intercepts the call → runs logic → calls the original.
🔍 @decorator Is Just Syntax Sugar
Python rewrites:
@announce
def greet():
...
as:
def greet():
...
greet = announce(greet)
That’s all it is — assignment using decorator syntax.
def announce(func):
def wrapper():
print("Announcing...")
return func()
return wrapper
@announce
def greet():
print("Hi from greet()")
greet()
🧠 Decorators with Arguments Use Closures
To accept arguments, a decorator must form a closure — a function returning another function which returns another function.
Three layers:
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def hello():
print("Hello!")
The outer function captures n → the inner wrappers use it.
⚠️ Why functools.wraps Is Mandatory
Without wraps, decorated functions lose their identity.
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def square(x):
"""Return x squared."""
return x * x
print(square.__name__)
print(square.__doc__)
print(inspect.signature(square))
Output:
wrapper
None
(*args, **kwargs)
Frameworks depend heavily on metadata
→ missing wraps breaks debuggers, stack traces, IDEs, and auto-doc tools.
✨ functools.wraps Fixes Everything
Using wraps preserves:
- name
- doc
- Type hints
- Signature
- Module
- Metadata
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def square(x):
"""Return x squared."""
return x * x
Now metadata is correct.
🧱 Stacked Decorators Execute Bottom-Up
@A
@B
def hello():
...
becomes:
hello = A(B(hello))
Example:
def A(func):
@functools.wraps(func)
def wrapper():
print("A start")
func()
print("A end")
return wrapper
def B(func):
@functools.wraps(func)
def wrapper():
print("B start")
func()
print("B end")
return wrapper
@A
@B
def hello():
print("Hello!")
Output:
A start
B start
Hello!
B end
A end
Decorators wrap from the bottom up — important in frameworks.
✅ Key Points
- Decorators = function wrappers powered by closures
- @decorator is simply f = decorator(f)
- Without wraps, metadata breaks (name, doc, signature)
- With wraps, decorated functions behave like originals
- Multiple decorators wrap bottom-up and execute top-down
Decorators power a huge portion of Python’s internal ecosystem — understanding them is essential to writing clean, extensible code.
Code Snippet:
import functools # for preserving metadata of wrapped functions
import inspect # to inspect signatures and metadata
def simple_decorator(func):
def wrapper():
print("Before call")
func()
print("After call")
return wrapper
def greet():
print("Hello!")
decorated = simple_decorator(greet)
decorated()
def announce(func):
def wrapper():
print("Announcing...")
return func()
return wrapper
@announce # equivalent to greet = announce(greet)
def greet():
print("Hi from greet()")
greet()
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def hello():
print("Hello!")
hello()
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def square(x):
"""Return x squared."""
return x * x
print(square.__name__) # wrong: "wrapper"
print(square.__doc__) # lost!
print(inspect.signature(square)) # loses signature
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def square(x):
"""Return x squared."""
return x * x
print(square.__name__) # correct: "square"
print(square.__doc__) # preserved
print(inspect.signature(square)) # real signature
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
duration = time.perf_counter() - start
print(f"{func.__name__} took {duration:.6f}s")
return result
return wrapper
@timer
def compute():
sum(i*i for i in range(10_000))
compute()
def A(func):
@functools.wraps(func)
def wrapper():
print("A start")
func()
print("A end")
return wrapper
def B(func):
@functools.wraps(func)
def wrapper():
print("B start")
func()
print("B end")
return wrapper
@A
@B
def hello():
print("Hello!")
hello()
No comments yet. Be the first to comment!