🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python Decorators

Understand decorators from first principles — functions wrapping functions — and use functools.wraps, parameterized decorators, and class decorators like a professional.

Total XP: 0|💻 python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What does @timer above def say_hello(): actually do?


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

A decorator is nothing more than a function that takes a function and returns a function. That single idea, once it clicks, explains @property, @staticmethod, @app.route, @pytest.fixture, and every other @-prefixed line you've ever copied without fully understanding.

1A Decorator Is Just a Function That Returns a Function

The entirety of what a decorator is can be demonstrated without the @ syntax at all: timer is an ordinary function that accepts another function, func, and returns a brand new function, wrapper, which calls func and adds behavior around that call. say_hello = timer(say_hello) reassigns the name say_hello to point at wrapper instead of the original function — the original function object still exists, just captured inside wrapper's closure.

@timer written directly above def say_hello(): is defined by the language to mean exactly that reassignment, evaluated immediately after the function body is compiled. There is no additional mechanism, no special runtime hook — it's syntactic sugar for func = decorator(func), which is precisely why any callable that accepts one function argument and returns one callable can be used as a decorator, including classes with a __call__ method.

This is also why decorators compose in a predictable, textually-readable order: @a @b def f(): is equivalent to f = a(b(f))b wraps f first, then a wraps the result, and calling the final f() runs a's wrapper code, which calls b's wrapper code, which calls the original f.

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

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

say_hello = timer(say_hello)  # manual decoration
say_hello()
localhost:3000
Console Output
say_hello()
Hello!
say_hello took 0.0001s

2functools.wraps: Preserving Identity Through the Wrapper

The moment wrapper replaces the original function, every piece of metadata that made the original identifiable — __name__, __doc__, __module__, and the function's signature as introspected by tools — now belongs to wrapper, not to the function you actually decorated. Calling say_hello.__name__ after decoration returns 'wrapper', which is actively misleading in tracebacks, in help(say_hello), and in any tool (like a web framework's route table, or Sphinx documentation generation) that relies on introspecting the function's real name or docstring.

functools.wraps(func), applied as a decorator to wrapper itself, copies func's __name__, __doc__, __module__, __dict__, and a few other attributes onto wrapper, and also sets wrapper.__wrapped__ = func, which lets introspection tools (and inspect.signature) find the original function's real signature even through the wrapping layer.

Omitting @wraps is a distinctive beginner tell in decorator code, and it's not merely cosmetic: debugging a stack trace that shows wrapper instead of the actual function name that failed, or discovering that a framework's automatic API documentation shows every decorated endpoint as wrapper(*args, **kwargs), are both real, time-costing consequences of skipping it.

+
@timer
def say_hello():
    print("Hello!")

# identical to: say_hello = timer(say_hello)
say_hello()
localhost:3000
Introspection
say_hello.__name__ (with @wraps)
'say_hello' — identity preserved

3Parameterized Decorators: A Function Returning a Decorator

@timer and @retry(times=3) look similar but differ in one crucial way: @timer applies timer directly to the function, while @retry(times=3) first calls retry(3) — an ordinary function call, evaluated immediately — and *whatever that call returns* becomes the decorator that's actually applied. This means retry itself is not a decorator; it's a *decorator factory*, a function whose job is to build and return the real decorator, now closed over the times argument.

That's why the parameterized version needs three nested levels instead of two: retry(times) returns decorator, decorator(func) returns wrapper, and wrapper(*args, **kwargs) is what actually executes when the decorated function is called. Each layer's closure carries forward exactly the state it needs — decorator remembers times from its enclosing scope, and wrapper remembers both times and func.

A subtlety worth internalizing: because retry(times=3) must always be called with parentheses — even with no arguments, as @retry() — a common ergonomic pattern is making the decorator work both bare (@retry) and called (@retry(times=3)) by checking, inside the outer function, whether it was called with a single callable argument. Most production decorators skip that complexity and simply require the parentheses consistently, favoring predictability over cleverness.

+
print(say_hello.__name__)  # 'wrapper', not 'say_hello' — misleading!

from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
localhost:3000
Call Chain
@retry(times=3)
retry(3) → decorator → decorator(flaky_call) → wrapper

4Step-by-Step Breakdown

Every '@' symbol above a function definition is calling a function on a function. Let's build that mental model from scratch.

Strip away the @ syntax and a decorator is just this: timer(say_hello) returns a new function that wraps the original.

@timer above a function definition is pure syntactic sugar for exactly that reassignment — nothing more.

Checkpoint: What does @timer above def say_hello(): actually do?

  • Reassigns say_hello to the result of timer(say_hello)
  • Runs say_hello in a background thread automatically

Without functools.wraps, the decorated function loses its real name and docstring — a common source of confusing debugging sessions.

A decorator that takes its own arguments needs an extra layer: a function that returns a decorator.

Checkpoint: Why does a parameterized decorator like @retry(times=3) need an extra nested function level compared to @timer?

  • @retry(times=3) first calls retry(3), which must return the actual decorator function
  • It does not — parameterized and non-parameterized decorators are identical

Decorators wrap callables — generators, our next stop, let you build lazy, memory-efficient iterables using the same 'function that isn't quite a function' intuition.

Build a Real Logging Decorator. Finish log_call(): a decorator wraps a function to add behavior before/after the original call.

Level Up 🚀

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Always apply @functools.wraps(func) to your wrapper function

It costs one line and preserves __name__, __doc__, and introspectability — skipping it silently corrupts debugging, documentation generation, and any framework that inspects the decorated function.

Keep decorators single-purpose and composable

A @timer that only times and a @retry that only retries can be stacked (@timer @retry(times=3) def f(): ...) far more flexibly than one decorator that tries to time, retry, and log all at once.

Frequent Bugs

THE BUG

Forgetting *args, **kwargs on wrapper, breaking any decorated function that takes arguments other than the exact ones the decorator's author anticipated.

THE FIX

Write wrapper as def wrapper(*args, **kwargs): and forward them with func(*args, **kwargs) unless you have a specific, documented reason to restrict the decorated function's signature.

Real-World Examples

A Simple Permission-Check Decorator for a Web Handler

A Flask/FastAPI-style handler function should only execute if the current user has the "admin" role, otherwise it should raise an error before the handler body runs.

from functools import wraps

def require_admin(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_admin"):
            raise PermissionError("Admin access required")
        return func(user, *args, **kwargs)
    return wrapper

@require_admin
def delete_account(user, account_id: int) -> None:
    print(f"Deleting account {account_id}")

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Omitting functools.wraps, so every decorated function reports __name__ as "wrapper" in tracebacks, logs, and auto-generated API docs.

# Wrong: identity lost def timer(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper # Correct from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper

The Solution //

Add @wraps(func) directly above the def wrapper(...) line inside every decorator you write.

Lesson Glossary

[01]Decorator

A callable that takes a function (or class) and returns a replacement callable, typically adding behavior around the original.

Code Preview
// Decorator context

[02]functools.wraps

A decorator used inside a wrapper function to copy the original function's __name__, __doc__, and other metadata onto the wrapper.

Code Preview
// functools.wraps context

[03]Closure

A nested function that captures and remembers variables from its enclosing function's scope, even after that outer function has returned.

Code Preview
// Closure context

[04]Decorator factory

A function that takes arguments and returns a decorator, enabling parameterized decorators like @retry(times=3).

Code Preview
// Decorator factory context

Continue Learning