Tutorials Logic, IN info@tutorialslogic.com

Decorators in Python @decorator

Python Decorators

A decorator wraps a function so extra behavior can run before, after, or around the original function call.

The @ syntax is shorthand for passing a function into another function and storing the returned wrapper under the same name.

Decorators are useful for logging, timing, permissions, caching, retries, and validation when the wrapping behavior is reusable.

Python Decorator

A decorator is a function that wraps another function to extend or modify its behavior - without changing the original function's code. They use the @ syntax.

Decorator Flow

Basic Decorator

Basic Decorator
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function")
        result = func(*args, **kwargs)
        print("After the function")
        return result
    return wrapper

# Apply with @ syntax
@my_decorator
def say_hello(name: str):
    print(f"Hello, {name}!")

say_hello("Alice")
# Before the function
# Hello, Alice!
# After the function

# Equivalent to:
# say_hello = my_decorator(say_hello)

Function Metadata

functools.wraps

functools.wraps
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

print(greet.__name__)
print(greet.__doc__)
Output
greet
Greet someone by name.

functools.wraps copies the original function metadata onto the wrapper, so debugging and documentation still show greet instead of wrapper.

Decorator Examples

Python Decorators Applied Examples

Python Decorators Applied Examples
import time
from functools import wraps

# Timer decorator
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.1)
    return "done"

slow_function()   # slow_function took 0.1001s

# Logger decorator
def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a: int, b: int) -> int:
    return a + b

add(3, 5)
# Calling add((3, 5), {})
# add returned 8

# Retry decorator
def retry(times: int = 3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"Attempt {attempt} failed: {e}")
            raise RuntimeError(f"Failed after {times} attempts")
        return wrapper
    return decorator

@retry(times=3)
def unstable_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network error")
    return "Success"

Stacking Decorators

Multiple Decorators

Multiple Decorators
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

# Applied bottom-up: italic first, then bold
@bold
@italic
def greet(name: str) -> str:
    return f"Hello, {name}!"

print(greet("Alice"))   # <b><i>Hello, Alice!</i></b>

# Class-based decorator
class Cache:
    def __init__(self, func):
        wraps(func)(self)
        self.func = func
        self._cache = {}

    def __call__(self, *args):
        if args not in self._cache:
            self._cache[args] = self.func(*args)
        return self._cache[args]

@Cache
def fibonacci(n: int) -> int:
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(35))   # fast due to caching
Decorator readiness check

Can You Wrap Behavior Safely?

5 checks
  • Explain that @decorator replaces the function with a wrapped version.
  • Preserve the wrapped function's metadata with functools.wraps.
  • Keep wrapper arguments flexible with *args and **kwargs.
  • Use decorators for reusable cross-cutting behavior such as logging or timing.
  • Avoid decorators when they hide behavior a beginner needs to see directly.

Wrapper Bugs to Catch

  • Changing function behavior without clarity

    Use decorators for cross-cutting behavior such as timing, logging, auth, or caching.
  • Forgetting to return the wrapper result

    If the original function returns a value, the wrapper should usually return it too.
  • Losing function metadata

    Apply functools.wraps to production decorators so names, documentation, and introspection remain accurate.

Try this next

Wrap a Function Safely

0 of 3 completed

  1. Write a decorator that prints before and after a function runs.
  2. Decorate an add function and confirm the returned sum is still available.
  3. Add functools.wraps, then compare the decorated function name and documentation before and after the change.

Questions About Decorator

@decorator above a function is shorthand for function_name = decorator(function_name).

wraps preserves the original function name, docstring, and metadata so debugging and documentation stay useful.

If the wrapper hides important behavior or is used only once, a normal helper function may be easier to read.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.