Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Python
  3. Python Decorators Practice: wraps, factories, stacking order & lru_cache
Python9 exercises

Python Decorators Practice: wraps, factories, stacking order & lru_cache

Learn decorators without copy-pasting templates: the wrapper pattern, @functools.wraps, decorators with arguments, stacking order (g(f(x))), and stdlib decorators like @property/@classmethod plus @lru_cache.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Write a function `get_item()` (no params, body is `pass`) with the `@login_required` decorator applied

On this page
  1. 1Decorators are function reassignment
  2. 2Stacking decorators: bottom-up application
  3. 3The 3-layer template: decorator factories
  4. 4Always use functools.wraps
  5. 5Timing decorator: the right way
  6. 6Decorating methods: self comes through *args
  7. 7Decorating async functions
  8. 8Stdlib decorators you should know
  9. 9Class decorators
  10. 10References
Decorators are function reassignmentStacking decorators: bottom-up applicationThe 3-layer template: decorator factoriesAlways use functools.wrapsTiming decorator: the right wayDecorating methods: self comes through *argsDecorating async functionsStdlib decorators you should knowClass decoratorsReferences

Python decorators look like magic until the syntax clicks:

  • @dec above def f(...) is just f = dec(f)
  • Stacking matters: @g above @f means f = g(f(original)) (bottom decorator applied first)

These exercises cover the patterns you'll actually use:

  • Wrapper functions with *args, **kwargs
  • @functools.wraps (so tooling shows the original name/docs/signature)
  • Decorator factories (decorators with arguments)
  • Practical built-ins like @property, @classmethod, @staticmethod
  • Common stdlib decorators like @lru_cache for memoization
Related Python Topics
Python Function Arguments: defaults, *args, keyword-only, **kwargsPython Classes: self, super(), __repr__ vs __str__, inheritancePython Comprehensions: List, Dict, Set & Generator Expressions

@decorator above def f() is just f = decorator(f). A decorator receives a function and returns a (usually modified) function.

Here's the core idea:

@decorator
def func():
    pass

is exactly equivalent to:

def func():
    pass
func = decorator(func)

A decorator is just a function that takes a function and returns a (usually modified) function. That's it. This wrapping principle is an instance of the Decorator design pattern — a general OOP concept where you wrap an object to add behavior without modifying it.


Ready to practice?

Start practicing Python Decorators: wraps, factories, stacking order & lru_cache with spaced repetition

Bottom decorator wraps first. @g above @f means g(f(original)). When called, the outer decorator's logic runs first.

When you stack decorators, the bottom one wraps first:

@auth_required
@log_calls
def delete_user(user_id):
    ...

Expands to:

delete_user = auth_required(log_calls(delete_user))

Decorator stacking order: wrapping happens bottom-up at definition time, but execution runs top-down at call time

The inner decorator (log_calls) wraps the original function. The outer decorator (auth_required) wraps that result. When you call delete_user(), auth runs first, then logging, then the original.


A decorator with arguments needs three layers: factory(args) returns decorator(func) returns wrapper(*args, **kwargs). The @factory(...) call returns the actual decorator.

A decorator that takes arguments needs an extra layer:

def factory(arg1, arg2):           # 1. Factory captures arguments
    def decorator(func):            # 2. Decorator receives the function
        @wraps(func)
        def wrapper(*args, **kwargs):  # 3. Wrapper does the work
            # use arg1, arg2, func here
            return func(*args, **kwargs)
        return wrapper
    return decorator

@factory("value1", "value2")
def my_func():
    pass

Three nested layers of a decorator factory: factory captures arguments, decorator receives the function, wrapper does the work

@factory(...) is a function call that returns a decorator. That decorator then wraps my_func.


Without @wraps(func), your wrapper hides the original function's __name__, __doc__, and __signature__. Always add it inside your wrapper.

Without @wraps(func), your wrapper hides the original function's identity:

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def greet():
    """Say hello."""
    pass

print(greet.__name__)  # "wrapper" - wrong!
print(greet.__doc__)   # None - lost the docstring!

functools.wraps before and after: without wraps, name returns 'wrapper' and doc is None; with wraps, original metadata is preserved

With @wraps(func):

from functools import wraps

def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def greet():
    """Say hello."""
    pass

print(greet.__name__)  # "greet" - correct!
print(greet.__doc__)   # "Say hello." - preserved!

This matters for debugging, documentation tools, and inspect.signature(). For a copy-paste reference of decorator syntax patterns, see the Python decorators cheat sheet.


Use time.perf_counter() for measuring durations—it's high-resolution and monotonic (won't jump if the system clock changes):

from functools import wraps
from time import perf_counter

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

The try/finally ensures timing is logged even if the function raises an exception.


When decorating a method, remember that self (or cls for classmethods) arrives as the first positional argument:

def log_method(func):
    @wraps(func)
    def wrapper(*args, **kwargs):  # self is in args[0]
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

class Calculator:
    @log_method
    def add(self, a, b):
        return a + b

If you wrote wrapper(a, b) instead of wrapper(*args, **kwargs), it would fail because it doesn't accept self.


A sync wrapper on an async function doesn't await—it just returns a coroutine object:

# BROKEN: sync wrapper on async function
def broken_timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        result = func(*args, **kwargs)  # Returns coroutine, doesn't run it!
        print(f"Took {perf_counter() - start}s")  # Lies: measures nothing
        return result
    return wrapper

For async functions, use async def and await:

def async_timer(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        start = perf_counter()
        try:
            return await func(*args, **kwargs)  # Actually runs the coroutine
        finally:
            print(f"{func.__name__} took {perf_counter() - start:.4f}s")
    return wrapper

@property - Turn a method into a read-only attribute:

@property
def full_name(self):
    return f"{self.first} {self.last}"

@classmethod - Method receives the class, not an instance:

@classmethod
def from_json(cls, data):
    return cls(**json.loads(data))

@staticmethod - No implicit first argument (just a namespaced function):

@staticmethod
def is_valid_email(email):
    return "@" in email

@functools.lru_cache - Memoize expensive function calls:

@lru_cache(maxsize=128)
def expensive_computation(n):
    ...

@functools.cache (Python 3.9+) - Unbounded memoization:

@cache
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)

Decorators can also wrap classes, not just functions. The pattern is the same: the decorator receives the class and returns a (possibly modified) class.

@dataclass - The most common class decorator, auto-generates boilerplate:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# Equivalent to writing __init__, __repr__, __eq__ yourself
p = Point(1.0, 2.0)
print(p)  # Point(x=1.0, y=2.0)

Custom class decorator - Register or modify classes:

registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class MyPlugin:
    pass

# registry == {'MyPlugin': <class 'MyPlugin'>}

Class decorators are useful for ORMs, plugin systems, and reducing boilerplate. See the OOP guide for more on how @dataclass and class decorators fit into Python's object model.


  • functools.wraps documentation
  • functools.lru_cache documentation
  • PEP 318 – Decorators for Functions and Methods
  • Built-in Functions: classmethod, staticmethod, property
  • time.perf_counter (high-resolution timing)
  • inspect.signature with follow_wrapped

When to Use Python Decorators: wraps, factories, stacking order & lru_cache

  • Add cross-cutting behavior like logging, caching, retries, timing, or input validation without changing call sites.
  • Enforce preconditions or permissions before executing a function.
  • Wrap a function to add metrics/tracing while keeping the original API.
  • Prefer stdlib decorators when they fit (e.g., @property, @classmethod, @lru_cache) before writing your own.

Check Your Understanding: Python Decorators: wraps, factories, stacking order & lru_cache

Prompt

Write a decorator that times a function call.

What a strong answer looks like

Wrap the function, measure with time.perf_counter() (high-resolution), return the result, and use functools.wraps to preserve metadata.

What You'll Practice: Python Decorators: wraps, factories, stacking order & lru_cache

Reading decorator syntax as reassignment: f = decorator(f)Writing basic decorators (func → wrapper)Preserving metadata with @functools.wrapsDecorator factories (decorators with arguments)Stacking multiple decorators and reasoning about orderUsing built-in decorators (@property, @classmethod, @staticmethod)Using stdlib caching decorators (@lru_cache / @cache)Decorating methods and async functions safelyClass decorators

Common Python Decorators: wraps, factories, stacking order & lru_cache Pitfalls

  • Forgetting @functools.wraps (tooling shows wrapper name/docs instead of the original)
  • Decorator order bugs when stacking (g(f(x)) vs f(g(x)))
  • Not handling *args/**kwargs so the wrapper breaks for different signatures
  • Using time.time() for timing (prefer perf_counter for durations)
  • Decorating async functions with a sync wrapper (you must await inside async def)
  • Forgetting to return the wrapper (you return None and break the function)
  • Methods: forgetting that wrapper receives self/cls via *args

Python Decorators: wraps, factories, stacking order & lru_cache FAQ

Why use functools.wraps?

It copies useful metadata from the wrapped function (like __name__ and __doc__) onto the wrapper, helping introspection tools and debuggers behave sensibly.

What does "stacking order" mean?

Decorators are applied bottom-to-top: @g above @f means the function becomes g(f(original)). The bottom decorator wraps first.

How do decorators with arguments work?

They are "decorator factories": an outer function captures parameters and returns the real decorator. Three layers: factory(args) → decorator(func) → wrapper(*args, **kwargs).

What's the difference between @classmethod and @staticmethod?

@classmethod receives cls as the first argument (can access class state); @staticmethod receives no implicit first argument (just a namespaced function).

Why does my decorated function show the wrong name?

You forgot @functools.wraps(func) in your wrapper. Without it, __name__, __doc__, and __signature__ reflect the wrapper, not the original function.

Can I decorate async functions?

Yes, but your wrapper must be async def and use await func(...). A sync wrapper just returns a coroutine object instead of executing the function.

What does functools.wraps do?

`@functools.wraps(func)` preserves the original function's metadata (name, docstring, module) when you wrap it with a decorator. Without it, the wrapper function replaces the original's identity, making debugging and introspection harder.

Python Decorators: wraps, factories, stacking order & lru_cache Syntax Quick Reference

Mental model (stacking)
@g
@f
def foo():
    ...

# Equivalent:
# foo = g(f(foo))
Timing decorator (perf_counter + wraps)
from functools import wraps
from time import perf_counter

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print(f"{func.__name__} took {perf_counter() - start:.4f}s")
    return wrapper
Decorator factory (with arguments)
from functools import wraps

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

@repeat(3)
def greet():
    print("Hello!")
Stdlib caching decorator
from functools import lru_cache

@lru_cache(maxsize=256)
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)
Async function decorator
from functools import wraps

def log_calls(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return await func(*args, **kwargs)
    return wrapper
Built-in decorators
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

    @classmethod
    def from_diameter(cls, d):
        return cls(d / 2)

    @staticmethod
    def is_valid_radius(r):
        return r > 0
Class decorator (@dataclass)
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# Generates __init__, __repr__, __eq__ automatically

Python Decorators: wraps, factories, stacking order & lru_cache Sample Exercises

Example 1Difficulty: 2/5

Write a decorator `log_call` that prints "Calling" before running any function. Use *args and **kwargs so it works with functions that take arguments.

def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling")
        return func(*args, **kwargs)
    return wrapper
Example 2Difficulty: 3/5

Fill in the decorator that preserves the wrapped function's metadata. (Assume `import functools` is done)

@functools.wraps(func)
Example 3Difficulty: 2/5

What does this code print?

Decorating
Hello

+ 6 more exercises

Quick Reference
Python Decorators: wraps, factories, stacking order & lru_cache Cheat Sheet →

Copy-ready syntax examples for quick lookup

Further Reading

  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Related Design Patterns

Decorator Pattern

Start practicing Python Decorators: wraps, factories, stacking order & lru_cache

Free daily exercises with spaced repetition. No credit card required.

← Back to Python Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.