AW Dev Rethought

⚖️ There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies - C.A.R. Hoare

🧠 Python DeepCuts — 💡 Under the Hood of Decorators


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()

Link copied!

Comments

Add Your Comment

Comment Added!