How To Guide

Python Decorators: A Complete Tutorial with Real Examples

Complete Python decorators tutorial covering syntax, arguments, wraps, class decorators, and built-ins.

{ "slug": "python-decorator-tutorial", "title": "Python Decorators: A Complete Tutorial with Real Examples", "description": "Learn Python decorators from scratch: @syntax, decorators with arguments, functools.wraps, class decorators, and built-ins like @property and @lru_cache.", "type": "how-to", "primaryTool": "regex-tester", "toolSlugs": ["regex-tester", "json-formatter", "code-beautifier"], "keywords": ["python decorators", "functools wraps", "decorator with arguments", "class decorator", "property decorator", "lru_cache", "python 3.10", "syntactic sugar"], "steps": [ {"name": "Understand the concept", "text": "A decorator is a callable that takes a function and returns a new function. The @ symbol is just syntactic sugar for reassignment."}, {"name": "Write your first decorator", "text": "Build a logging decorator using *args and **kwargs so it works on any function signature."}, {"name": "Add arguments to your decorator", "text": "Wrap the decorator in another function to accept parameters, giving you three levels of nesting."}, {"name": "Preserve metadata with functools.wraps", "text": "Always apply @wraps to your inner wrapper so __name__, __doc__, and signatures survive."}, {"name": "Use class decorators for state", "text": "Implement decorators as classes with __call__ when you need persistent state like call counts."}, {"name": "Master the built-in decorators", "text": "Reach for @property, @staticmethod, @classmethod, @cached_property, @lru_cache, and @dataclass before rolling your own."} ], "content": "

Python Decorators: A Complete Tutorial

\n

Decorators are one of Python's most elegant features, yet they trip up newcomers because they involve functions that return functions that return functions. This guide walks through the mental model, then covers everything from a simple logging decorator to the standard-library workhorses you'll use every day. All examples target Python 3.10+.

\n\n

What a decorator actually is

\n

A decorator is just a callable that takes a function and returns a new (usually wrapped) function. The @decorator line above a definition is syntactic sugar for reassigning the name:

\n
@my_decorator\ndef greet(): ...\n\n# is exactly equivalent to:\ndef greet(): ...\ngreet = my_decorator(greet)
\n

That's it. Everything else is a variation on that theme.

\n\n

Your first decorator: logging

\n
def log_calls(func):\n    def wrapper(*args, **kwargs):\n        print(f\"calling {func.__name__}({args}, {kwargs})\")\n        result = func(*args, **kwargs)\n        print(f\"{func.__name__} returned {result!r}\")\n        return result\n    return wrapper\n\n@log_calls\ndef add(a, b):\n    return a + b\n\nadd(2, 3)\n# calling add((2, 3), {})\n# add returned 5
\n

The *args, **kwargs pattern lets your wrapper accept any signature. Always forward them to func unchanged.

\n\n

Decorators with arguments (three levels of nesting)

\n

When you want @repeat(times=3), you need an extra outer layer. The outer function accepts the decorator arguments and returns the actual decorator:

\n
from functools import wraps\n\ndef repeat(times: int = 1):\n    def decorator(func):\n        @wraps(func)\n        def wrapper(*args, **kwargs):\n            for _ in range(times):\n                result = func(*args, **kwargs)\n            return result\n        return wrapper\n    return decorator\n\n@repeat(times=3)\ndef ping():\n    print(\"pong\")\n\nping()  # prints 'pong' three times
\n

Read it inside-out: repeat(times=3) is called first, returns decorator, which is then applied to ping.

\n\n

Preserve metadata with functools.wraps

\n

Without @wraps, decorated functions lose their __name__, __doc__, and signature, breaking introspection, help(), and debuggers:

\n
from functools import wraps\n\ndef timing(func):\n    @wraps(func)  # copies name, doc, __wrapped__, annotations\n    def wrapper(*args, **kwargs):\n        import time\n        start = time.perf_counter()\n        try:\n            return func(*args, **kwargs)\n        finally:\n            elapsed = time.perf_counter() - start\n            print(f\"{func.__name__} took {elapsed:.4f}s\")\n    return wrapper\n\n@timing\ndef slow():\n    \"\"\"A slow function.\"\"\"\n    sum(range(10_000_000))\n\nprint(slow.__name__)  # 'slow', not 'wrapper'\nprint(slow.__doc__)   # 'A slow function.'
\n\n

Class decorators (stateful)

\n

When you need persistent state, a class with __call__ is often cleaner than closures:

\n
class CountCalls:\n    def __init__(self, func):\n        wraps(func)(self)  # copy metadata onto self\n        self.func = func\n        self.count = 0\n    def __call__(self, *args, **kwargs):\n        self.count += 1\n        return self.func(*args, **kwargs)\n\n@CountCalls\ndef hello(name):\n    return f\"hi {name}\"\n\nhello(\"Ada\"); hello(\"Bob\")\nprint(hello.count)  # 2
\n\n

Built-in decorators you should know

\n
    \n
  • @property - turns a method into a read-only attribute; pair with @name.setter for writes.
  • \n
  • @staticmethod - a namespaced function on a class, no self or cls.
  • \n
  • @classmethod - receives the class as cls; ideal for alternate constructors.
  • \n
  • @functools.cached_property - computes once, then stores the value on the instance.
  • \n
  • @functools.lru_cache(maxsize=128) - memoizes based on arguments; huge win for pure functions.
  • \n
  • @dataclasses.dataclass - auto-generates __init__, __repr__, __eq__.
  • \n
\n
from functools import lru_cache, cached_property\nfrom dataclasses import dataclass\n\n@lru_cache(maxsize=None)\ndef fib(n: int) -> int:\n    return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n@dataclass(frozen=True)\nclass Point:\n    x: float\n    y: float\n    @cached_property\n    def magnitude(self) -> float:\n        return (self.x ** 2 + self.y ** 2) ** 0.5
\n\n

Common pitfalls

\n
    \n
  1. Forgetting @wraps. Everything appears to work until a framework introspects your function - Flask routes, pytest fixtures, and FastAPI dependencies all break silently.
  2. \n
  3. Mutable default args in decorator factories. def cache(store={}): shares one dict across every decorated function. Use store=None and initialize inside.
  4. \n
  5. Applying decorators to methods before @classmethod/@staticmethod. Order matters - the class-modifying decorator goes on top.
  6. \n
  7. Returning the wrong thing. Your wrapper must return func(*args, **kwargs) or callers lose the value.
  8. \n
  9. Over-decorating. If a decorator has no wrapper and only registers the function, that's fine - but don't hide business logic inside layers of decoration.
  10. \n
\n

With these patterns in hand, decorators become a lightweight way to add cross-cutting behavior - logging, timing, caching, validation, registration - without touching the wrapped function's body.

" }

Featured Tools

Try these free tools directly in your browser — no sign-up required.

python decorators functools wraps decorator arguments class decorator lru_cache property decorator python 3.10 syntactic sugar

Explore 300+ Free Tools

Utilko has tools for developers, writers, designers, students, and everyday users — all free, all browser-based.