Python & Django

Decorators: Wrapping Functions with Style

August 17, 2026
2 min read
1 views

Decorators are one of Python's more "magic-looking" features — but they're really just functions that take a function and return a new one.

Starting from First Principles

Since functions are objects, you can pass them around and wrap them:

def shout(func):
    def wrapper():
        result = func()
        return result.upper()
    return wrapper

def greet():
    return "hello"

greet = shout(greet)
print(greet())  # "HELLO"

The @ syntax is just sugar for exactly this pattern:

@shout
def greet():
    return "hello"

print(greet())  # "HELLO"

@shout above def greet is equivalent to writing greet = shout(greet).

Handling Arguments

Real functions take arguments, so decorators typically use *args and **kwargs to forward whatever was passed in:

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

@timer
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(2, 3)
# slow_add took 1.0001s

Preserving Metadata with functools.wraps

Without care, a decorated function loses its original name and docstring:

print(slow_add.__name__)  # "wrapper" — not helpful!

functools.wraps fixes this:

from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.time() - start:.4f}s")
        return result
    return wrapper

Now slow_add.__name__ correctly reports "slow_add".

Decorators with Arguments

Sometimes you want to configure the decorator itself, which requires an extra layer of nesting:

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(times=3)
def say_hi():
    print("Hi!")

say_hi()
# Hi!
# Hi!
# Hi!

Common Real-World Uses

  • Logging: record every call to a function
  • Caching: functools.lru_cache memoizes results automatically
  • Access control: check permissions before running a view function (common in Flask/Django)
  • Retry logic: automatically retry a function on failure
  • Validation: check argument types or values before execution
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

Class-Based Decorators

Decorators don't have to be functions — any callable works, including classes with __call__:

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()
say_hello()

Key Takeaway

Decorators are just functions wrapping functions — a clean way to add behavior (timing, caching, logging, validation) without cluttering the core logic of the function itself. Once you see through the @ syntax to the underlying func = decorator(func) pattern, they stop feeling like magic.

Topics

Python
MAR

MD Abdur Rahim

Senior Python Developer helping teams ship backend systems and AI products — Django, FastAPI, LangChain, RAG pipelines, and cloud infra that hold up in production.

Comments (0)

Minimum 3 characters

0/1000

No comments yet

Be the first to share your thoughts!

Enjoyed this article?

Subscribe to my newsletter to receive updates on new blog posts, tech insights, and development tips.

No spam. Unsubscribe anytime. Read our Privacy Policy.