DEV Community

Deepika Pusala
Deepika Pusala

Posted on

Week 4 - Task 2.3 : From Functions to Powerful Decorators πŸπŸ’œ

Four concepts, one story: functions can be treated like values β†’ that lets inner functions remember things β†’ which lets us build decorators β†’ which can be made configurable. Let's walk through it end to end.

First-Class Functions
        ↓
Functions can be passed around & returned
        ↓
Closures become possible
        ↓
Closures help us build decorators
        ↓
Decorators can accept parameters
        ↓
functools.wraps preserves metadata
Enter fullscreen mode Exit fullscreen mode

1️⃣ First-Class Functions

In Python, a function is just another value β€” you can store it, pass it, or return it, exactly like an int or a string.

def greet():
    print("Hello!")

x = greet   # NOT greet() β€” just the function itself
x()         # Hello!
Enter fullscreen mode Exit fullscreen mode
Code Meaning
greet The function itself
greet() Executes the function
x = greet Stores the function in x
x = greet() Stores the return value of calling it

Passing a function as an argument

def greet():
    print("Hello!")

def execute(func):
    func()

execute(greet)   # Hello!
Enter fullscreen mode Exit fullscreen mode

Real use: a generic perform_action() that works with any function you hand it:

def login(): print("Logging in")
def logout(): print("Logging out")

def perform_action(action):
    action()

perform_action(login)
perform_action(logout)
Enter fullscreen mode Exit fullscreen mode

Returning a function

def outer():
    def inner():
        print("Hello from inner")
    return inner

x = outer()
x()   # Hello from inner
Enter fullscreen mode Exit fullscreen mode

This β€” a function that creates and returns another function β€” is the doorway into closures.


2️⃣ Closures πŸ”

A closure is a function that remembers variables from its surrounding scope, even after the outer function has already finished running.

Think of it like a parent passing a secret to their child before leaving β€” the child still remembers it long after the parent is gone.

def outer():
    name = "Deepika"

    def inner():
        print(name)

    return inner

func = outer()
func()   # Deepika
Enter fullscreen mode Exit fullscreen mode

outer() has already returned by the time we call func() β€” yet inner() still remembers name. That's the closure at work.

The 3 ingredients of a closure

# Requirement
1 A nested function
2 The inner function uses a variable from the outer scope
3 The outer function returns the inner function

A more useful example

def multiplier(number):
    def multiply(x):
        return x * number
    return multiply

double = multiplier(2)
triple = multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15
Enter fullscreen mode Exit fullscreen mode

double and triple are two separate closures β€” each remembers a different number.


3️⃣ Decorators 🎁

A decorator is a function that modifies or extends another function's behavior without changing its original code.

def decorator(func):
    def wrapper():
        print("Before function")
        func()
        print("After function")
    return wrapper

@decorator
def greet():
    print("Hello!")

greet()
Enter fullscreen mode Exit fullscreen mode
Before function
Hello!
After function
Enter fullscreen mode Exit fullscreen mode

What @decorator actually does

@decorator
def greet():
    print("Hello")
Enter fullscreen mode Exit fullscreen mode

is literally shorthand for:

def greet():
    print("Hello")

greet = decorator(greet)
Enter fullscreen mode Exit fullscreen mode

decorator(greet) returns wrapper, and greet now points to wrapper instead of the original function.


4️⃣ Parameterized Decorators 🎯

What if the decorator itself needs an argument?

@repeat(3)
def greet():
    print("Hello")
Enter fullscreen mode Exit fullscreen mode

Here repeat(3) isn't the function being decorated β€” it's configuring the decorator. That requires one extra layer of nesting.

def repeat(times):              # 1️⃣ receives the decorator's argument

    def decorator(func):        # 2️⃣ receives the actual function

        def wrapper():          # 3️⃣ runs the function
            for _ in range(times):
                func()

        return wrapper

    return decorator

@repeat(3)
def greet():
    print("Hello")

greet()
Enter fullscreen mode Exit fullscreen mode
Hello
Hello
Hello
Enter fullscreen mode Exit fullscreen mode

The three layers:

  • repeat(times) β€” captures times = 3
  • decorator(func) β€” captures func = greet
  • wrapper() β€” actually calls func(), times times

The key insight: it's a closure

wrapper() uses times, which belongs to repeat(times) β€” an outer function that has already returned. That's exactly the closure pattern from earlier.

repeat(3)
β”‚
β”œβ”€β”€ times = 3
β”‚
└── decorator()
      β”‚
      └── wrapper()
             β”‚
             └── remembers times = 3
Enter fullscreen mode Exit fullscreen mode

Parameterized decorators are built using closures. That's the connection worth remembering.

Another example β€” a message decorator:

def message(text):
    def decorator(func):
        def wrapper():
            print(text)
            func()
        return wrapper
    return decorator

@message("Welcome to Python!")
def greet():
    print("Hello Deepika")

greet()
Enter fullscreen mode Exit fullscreen mode
Welcome to Python!
Hello Deepika
Enter fullscreen mode Exit fullscreen mode

5️⃣ The Metadata Problem 😭

def greet():
    """This function greets the user."""
    print("Hello")

print(greet.__name__)   # greet
print(greet.__doc__)    # This function greets the user.
Enter fullscreen mode Exit fullscreen mode

Now decorate it:

def decorator(func):
    def wrapper():
        func()
    return wrapper

@decorator
def greet():
    """This function greets the user."""
    print("Hello")

print(greet.__name__)   # wrapper  😳
Enter fullscreen mode Exit fullscreen mode

Since greet = decorator(greet) makes greet point to wrapper, Python loses track of the original function's name and docstring.

The fix: functools.wraps

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper():
        func()
    return wrapper

@decorator
def greet():
    """This function greets the user."""
    print("Hello")

print(greet.__name__)   # greet
print(greet.__doc__)    # This function greets the user.
Enter fullscreen mode Exit fullscreen mode

@wraps(func) copies over metadata like __name__, __doc__, __module__, and __annotations__ from the original function onto the wrapper.

Why it matters: in a real app with 100 decorated functions, without @wraps every single one shows up as wrapper during debugging. With it, each keeps its real name β€” much easier to trace.


6️⃣ *args and **kwargs β€” Making Decorators Work with Any Function

A real decorator needs to handle functions with different signatures:

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Before function")
        result = func(*args, **kwargs)
        print("After function")
        return result
    return wrapper
Enter fullscreen mode Exit fullscreen mode

Now it works whether the decorated function is add(10, 20), greet("Deepika"), or login(username="deepika", password="1234"). This is why almost every real-world decorator you'll see includes *args, **kwargs.


🧩 Putting It All Together

from functools import wraps

def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet():
    """Greets the user."""
    print("Hello!")

greet()
print(greet.__name__)   # greet
print(greet.__doc__)    # Greets the user.
Enter fullscreen mode Exit fullscreen mode

This one example uses all four concepts:

  1. func β€” a function passed as a first-class value
  2. wrapper() remembering times β€” a closure
  3. @repeat(3) β€” a parameterized decorator
  4. @wraps(func) β€” preserving metadata

🌍 Where This Shows Up in Real Code

Application What decorators do
πŸ” Authentication Check whether a user is logged in
⏱️ Performance Measure execution time
πŸ“ Logging Record function calls
πŸ”‘ Authorization Check permissions
πŸ’Ύ Caching Store previous results (@lru_cache)
πŸ› Debugging Track function behavior
🌐 Web frameworks Route functions to URLs β€” e.g. @app.route("/home")
πŸ”„ Retry logic Retry failed operations

🎯 Interview-Ready Definitions

Question Answer
What is a first-class function? A function that can be stored in a variable, passed as an argument, and returned from another function β€” treated like any other value
What is a closure? An inner function that remembers and can access variables from its enclosing scope, even after the outer function has finished executing
What is a parameterized decorator? A decorator that accepts its own arguments/configuration before receiving the function it decorates, e.g. @repeat(3)
Why use functools.wraps? It preserves the original function's metadata (name, docstring, etc.) when a decorator replaces it with a wrapper

🧠 One Mental Model to Hold On To

FUNCTION
   β”‚  can be treated like a value
   β–Ό
FIRST-CLASS FUNCTION
   β”‚  can be returned
   β–Ό
INNER FUNCTION
   β”‚  remembers outer variables
   β–Ό
CLOSURE
   β”‚  used to build
   β–Ό
DECORATOR
   β”‚  accepts configuration
   β–Ό
PARAMETERIZED DECORATOR
   β”‚  use @wraps
   β–Ό
CLEAN + PRESERVED METADATA
Enter fullscreen mode Exit fullscreen mode

The go-to code skeleton for any configurable decorator:

from functools import wraps

def decorator(argument):
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # extra behavior before
            result = func(*args, **kwargs)
            # extra behavior after
            return result
        return wrapper
    return actual_decorator
Enter fullscreen mode Exit fullscreen mode

Once you can name what each of those three nested functions is doing, parameterized decorators stop looking scary. ❀️

Top comments (0)