Python Decorators: A Complete Tutorial with Real Examples
Complete Python decorators tutorial covering syntax, arguments, wraps, class decorators, and built-ins.
Python Decorators: A Complete Tutorial
\nDecorators 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\nWhat a decorator actually is
\nA 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:
@my_decorator\ndef greet(): ...\n\n# is exactly equivalent to:\ndef greet(): ...\ngreet = my_decorator(greet)\nThat's it. Everything else is a variation on that theme.
\n\nYour first decorator: logging
\ndef 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\nThe *args, **kwargs pattern lets your wrapper accept any signature. Always forward them to func unchanged.
Decorators with arguments (three levels of nesting)
\nWhen you want @repeat(times=3), you need an extra outer layer. The outer function accepts the decorator arguments and returns the actual decorator:
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\nRead it inside-out: repeat(times=3) is called first, returns decorator, which is then applied to ping.
Preserve metadata with functools.wraps
\nWithout @wraps, decorated functions lose their __name__, __doc__, and signature, breaking introspection, help(), and debuggers:
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\nClass decorators (stateful)
\nWhen you need persistent state, a class with __call__ is often cleaner than closures:
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\nBuilt-in decorators you should know
\n- \n
- @property - turns a method into a read-only attribute; pair with
@name.setterfor writes. \n - @staticmethod - a namespaced function on a class, no
selforcls. \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
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\nCommon pitfalls
\n- \n
- Forgetting @wraps. Everything appears to work until a framework introspects your function - Flask routes, pytest fixtures, and FastAPI dependencies all break silently. \n
- Mutable default args in decorator factories.
def cache(store={}):shares one dict across every decorated function. Usestore=Noneand initialize inside. \n - Applying decorators to methods before
@classmethod/@staticmethod. Order matters - the class-modifying decorator goes on top. \n - Returning the wrong thing. Your wrapper must return
func(*args, **kwargs)or callers lose the value. \n - 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. \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.
Regex Tester
Test and debug regular expressions in real time. Highlights matches, capture groups, and supports JavaScript regex flags for instant pattern validation.
JSON Formatter
Format, beautify, and validate JSON instantly. Paste raw JSON and get a clean, indented, human-readable output with syntax error detection.
Code Beautifier
Beautify and format code in JavaScript, TypeScript, HTML, CSS, JSON, and more. Auto-indent and clean up messy code with configurable style options.