Master Python Decorators: Syntax, Examples & Best Practices

Programming
Date:July 8, 2026
Topic:
Master Python Decorators: Syntax, Examples & Best Practices
2 min read

You write a function to log execution time. Then another for authentication. Then caching. Soon your business logic drowns in wrapper noise. Python decorators solve this by separating cross-cutting concerns from core logic, letting you compose behavior like LEGO bricks.

What Is a Decorator?

A decorator is a callable that takes a function, adds behavior, and returns a new function. Syntactically, @decorator above a definition equals func = decorator(func). This syntactic sugar enables metaprogramming without bytecode manipulation.

python
def timer(func):
    import time
    from functools import wraps

    @wraps(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

@timer
def slow_add(a, b):
    time.sleep(0.1)
    return a + b

slow_add(3, 5)  # prints timing, returns 8
💡
TipAlways use @functools.wraps to preserve the original function's metadata (__name__, __doc__, annotations).

Decorators with Arguments

Need configurable behavior? Add a factory layer. The outer function accepts arguments, returns the actual decorator.

python
def retry(max_attempts=3, delay=1.0):
    import time
    from functools import wraps

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=5, delay=0.5)
def flaky_api_call():
    ...

Class-Based Decorators

Classes implement __call__ for stateful decorators. Useful when you need to track call counts, cache results, or hold configuration.

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
        from functools import wraps
        wraps(func)(self)

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} to {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}"

greet("Ada")  # Call 1
greet("Alan") # Call 2

Stacking Multiple Decorators

Decorators apply bottom-to-top: the closest to def runs first. Order matters for logging, timing, and auth.

python
@timer
@retry(max_attempts=3)
@CountCalls
def fetch_data():
    ...
# Execution order: CountCalls -> retry -> timer
⚠️
WarningStacking changes behavior semantics. Test combinations in isolation before composing.

Real-World Patterns

PatternUse CaseStdlib Example
MemoizationCache expensive callsfunctools.lru_cache
RegistrationPlugin/plugin systemsFlask route()
Context InjectionDB sessions, request scopeFastAPI Depends
ValidationInput sanitizationPydantic validators
"

Decorators are the ultimate expression of Python's philosophy: explicit is better than implicit, but composition beats inheritance.

Raymond Hettinger

Best Practices Checklist

  • Use functools.wraps on every wrapper
  • Prefer parameterized decorators (factory pattern) over global config
  • Keep decorators pure: no side effects at import time
  • Document the contract: what exceptions propagate, what returns change
  • Type-hint with Callable[[P], R] and ParamSpec for static analysis
  • Avoid deep nesting; refactor to class if state grows


Open your editor. Find a function with duplicated setup/teardown logic. Extract that logic into a decorator today. Ship cleaner code tomorrow.

Share𝕏 Twitterin LinkedInin Whatsapp