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)