Python Decorators — Core Concepts

Decorators are one of Python’s most powerful patterns for cross-cutting behavior: things like logging, authentication, timing, retries, validation, and caching.

Instead of repeating that logic inside every function, you place it in one decorator and apply it where needed.

What a Decorator Really Does

This line:

@my_decorator
def get_user():
    ...

is equivalent to:

def get_user():
    ...

get_user = my_decorator(get_user)

my_decorator receives the original function and returns a new function (often called wrapper). Calls to get_user now go through that wrapper.

Basic Structure

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

*args and **kwargs make the decorator work with almost any function signature.

Common Use Cases

1) Authorization

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

2) Timing

import time

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

3) Caching

For expensive pure functions, decorators can memoize results. Standard library version: functools.lru_cache.

from functools import lru_cache

@lru_cache(maxsize=256)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

Preserving Function Metadata

Without help, wrappers replace function identity (__name__, docstring, signature). Use functools.wraps:

from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

This matters for debugging, documentation tools, and frameworks that inspect function signatures.

Decorators With Arguments

Sometimes you need options, like retry count.

from functools import wraps

def retry(times=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for _ in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
            raise last_error
        return wrapper
    return decorator

@retry(times=5)
def fetch_data():
    ...

Pattern:

  1. outer function gets config (times=5)
  2. middle function gets func
  3. inner function wraps execution

Common Misconception

Misconception: decorators are only for advanced framework magic.

Reality: decorators are practical in everyday scripts and apps whenever you see repeated “before/after function call” logic.

If you copy-paste the same pre-check or post-log into multiple functions, a decorator is often the cleanest refactor.

One Thing to Remember

A decorator is function composition with readable syntax: write reusable wrapper behavior once, then attach it to many functions with @....

pythondecoratorswrappersloggingauth

See Also

  • Python Async Await Async/await helps one Python program juggle many waiting jobs at once, like a chef who keeps multiple pots moving without standing still.
  • Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
  • Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
  • Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
  • Python Closures See how Python functions can remember private information, even after the outer function has already finished.