Decorators in Python 2026

Writing Functions Mar 16, 2026

Decorators in Python 2026

Decorators are one of Python’s most powerful and elegant features. A decorator is a function that takes another function as input, adds some behavior to it, and returns a new function. In 2026, decorators are widely used for logging, authentication, caching, timing, validation, and more.

TL;DR — Key Takeaways 2026

  • A decorator is a function that wraps another function to extend its behavior
  • Use the @decorator_name syntax for clean application
  • Always use functools.wraps to preserve original function metadata
  • Decorators can accept parameters (decorator factories)

1. Basic Decorator


from functools import wraps

def timer(func):
    """Decorator that measures execution time."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_operation(n: int):
    """Simulate a slow task."""
    import time
    time.sleep(0.1)
    return n * n

slow_operation(10)

2. Decorator with Parameters (Decorator Factory)


def repeat(times: int):
    """Decorator factory that repeats a function multiple times."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name: str):
    print(f"Hello, {name}!")

greet("Alice")

3. Best Practices for Decorators in 2026

  • Always use @wraps(func) from functools to preserve __name__, __doc__, and signature
  • Keep decorators simple and focused on a single responsibility
  • Use decorator factories when you need to pass parameters
  • Document what the decorator does and any side effects
  • Consider using libraries like wrapt for advanced decorator needs

What a Decorator Really Does

The @decorator line above a function is shorthand. Writing @timer above def load(): is the same as writing load = timer(load) after the definition. The decorator receives the function and returns a new callable that replaces it.

Most decorators return a wrapper function that does something before or after calling the original, such as timing, logging, caching or checking arguments. The wrapper should accept *args and **kwargs so it works with any signature.

functools.wraps copies the original name, docstring and other metadata to the wrapper. Without it, every decorated function would be called wrapper in tracebacks and help().

The standard library already offers useful decorators, such as functools.lru_cache, functools.cache and dataclasses.dataclass.

Worked Example: A Counting Decorator With wraps

We write a decorator that counts calls and show what wraps preserves.

import functools

def count_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def square(n):
    """Return n squared."""
    return n * n

print([square(i) for i in range(4)])
print(square.calls, square.__name__, square.__doc__)

@functools.lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(30), fib.cache_info().hits)

The decorated square returns [0, 1, 4, 9] and reports 4 calls, and wraps keeps the name square and its docstring. The cached fib(30) returns 832040 and the cache served 28 repeated calls.

Practical Tips

Keep decorators focused on one concern, such as timing, logging or caching. Stacking several small decorators is clearer than one decorator that does everything, and each one can be tested separately.

Remember that stacked decorators apply from the bottom up. The decorator nearest the def wraps the function first, and the top one wraps the result.

When a decorator must keep state, such as a call count, attach it to the wrapper as in the example or use a class with __call__.

lru_cache only works with hashable arguments, so pass tuples rather than lists to cached functions.

Apply decorators from the standard library first, such as functools.cache, before writing your own. They are well tested, fast and familiar to other Python developers.

Decorated functions should still be easy to test, so keep the original logic separate where needed.

Common Mistakes

  • Forgetting to return the wrapper from the decorator.
  • Leaving out functools.wraps.
  • Writing wrappers that accept only fixed arguments.
  • Re-implementing caching instead of using lru_cache.

Quick Summary

  • @dec is shorthand for f = dec(f).
  • Wrappers use *args and **kwargs.
  • functools.wraps keeps metadata.
  • Use standard decorators when they fit.

Conclusion

Decorators are a cornerstone of modern Python development. In 2026, mastering decorators allows you to write clean, reusable, and powerful code by separating cross-cutting concerns from your core business logic. From simple timing and logging to complex authentication and caching, decorators remain one of Python’s most beloved features.

Next steps:

  • Review your codebase and identify repeated behavior that can be replaced with decorators

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices