NotesPythonModules & Advanced

Decorators

Functions that wrap other functions — how @decorator syntax actually works.

In Python, functions are first-class objects — you can pass them as arguments, return them from other functions, and assign them to variables, exactly like any other value. Decorators are built entirely on that idea.

A decorator is a function that takes a function and returns a new function that usually calls the original, plus does something extra around it.

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hello, {name}"

print(greet("CodeWithMunnaX"))   # HELLO, CODEWITHMUNNAX

@shout above def greet is exactly equivalent to writing greet = shout(greet) right after defining it — the @ syntax is just a readable shorthand for "wrap this function with that decorator."

A practical example: timing a function

import time

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

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(1_000_000)

*args, **kwargs in the wrapper is what lets a single decorator work on any function, regardless of how many arguments it takes.

Preserving function metadata with functools.wraps

Without help, a decorated function loses its original name and docstring (it becomes wrapper). functools.wraps fixes that — it's considered good practice to always include it.

from functools import wraps

def shout(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@shout
def greet(name):
    """Return a friendly greeting."""
    return f"hello, {name}"

print(greet.__name__, "-", greet.__doc__)   # greet - Return a friendly greeting.

Where you'll see this pattern in the wild

Web frameworks use decorators constantly — @app.route("/") in Flask, @login_required for auth checks, @property for computed attributes on a class (covered in OOP).

Key points to remember
  • Functions are first-class objects in Python — they can be passed around, returned, and assigned like any other value, which is what makes decorators possible.
  • @decorator above a function definition is shorthand for func = decorator(func).
  • A decorator's inner wrapper typically takes *args, **kwargs so it works with any function signature.
  • functools.wraps(func) preserves the original function's name and docstring on the wrapped version — always use it.
  • This exact pattern powers things like Flask's @app.route and property getters — recognizing it helps you read real-world frameworks.

A timing decorator

Revision Flashcards

1 / 3

Tap a card to flip it and see the answer.