Master Code
On The Go

Learn. Practice. Build.

Home › How-To › Python Decorators

Python Guide

Python Decorators Explained

The most elegant way to add functionality to functions without touching their code.

Quick answer: A decorator is a function that takes another function as an argument, wraps it in a new function, and returns the wrapper. Use the @decorator syntax above a function to apply it. Use functools.wraps inside your wrapper to preserve the original function's metadata.

1

Functions Are First-Class Objects

Before understanding decorators, you must understand that in Python, functions can be passed around like any other value:

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

# Assign to a variable
say_hi = greet
print(say_hi("Alice"))  # Hello, Alice!

# Pass as argument
def run(func, arg):
    return func(arg)

print(run(greet, "Bob"))  # Hello, Bob!

This is what makes decorators possible.

2

Your First Decorator

Here is a decorator that times how long a function takes to run:

import time
from functools import wraps

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

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

slow_function()
# Output: slow_function took 1.0021s

The @timer syntax is equivalent to writing slow_function = timer(slow_function).

3

Why Use functools.wraps

Without @wraps, the decorated function loses its identity:

# Without @wraps
def bad_decorator(func):
    def wrapper():
        return func()
    return wrapper

@bad_decorator
def my_func():
    """My docstring"""
    pass

print(my_func.__name__)  # 'wrapper' ❌
print(my_func.__doc__)   # None ❌

# With @wraps
from functools import wraps

def good_decorator(func):
    @wraps(func)
    def wrapper():
        return func()
    return wrapper

@good_decorator
def my_func():
    """My docstring"""
    pass

print(my_func.__name__)  # 'my_func' ✅
print(my_func.__doc__)   # 'My docstring' ✅
4

Practical Decorators

Logging decorator:

from functools import wraps

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

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

add(2, 3)
# Calling add with (2, 3) {}

Retry decorator:

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

@retry(times=5)
def unstable_api_call():
    # Will be retried 5 times on failure
    pass
5

Stacking Decorators

You can apply multiple decorators to one function. They run bottom-up:

@timer
@log_calls
def process_data():
    return "result"

# Equivalent to:
# process_data = timer(log_calls(process_data))
#
# log_calls runs first, then timer

💡 Remember: The decorator closest to the function wraps first. If you want logging to time the log-then-run process, order matters.

6

Decorators with Arguments

To pass arguments to your decorator, wrap it in another function:

def repeat(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(times=3)
def say_hello():
    print("Hello!")

say_hello()
# Hello!
# Hello!
# Hello!

🛡️ Best Practices

Common mistake: Forgetting to return func(*args, **kwargs) inside the wrapper. This silently returns None from every decorated call — a bug that is hard to debug.

❓ Frequently Asked Questions

What is a decorator in Python?

A decorator is a function that takes another function as an argument and returns a modified version of it. Decorators let you add functionality to existing functions without changing their code.

When should I use decorators in Python?

Use decorators for cross-cutting concerns like logging, timing, caching, authentication, and validation. They keep your core logic clean by separating concerns.

What does functools.wraps do?

functools.wraps copies the metadata (name, docstring, annotations) of the wrapped function onto the wrapper. Without it, the decorated function loses its identity.

Can I use multiple decorators on one function?

Yes. Stack decorators on top of each other. They are applied bottom-up: the decorator closest to the function is applied first.

What is the difference between a decorator and a wrapper?

A decorator is the outer function that takes a function as input. A wrapper is the inner function that actually wraps the original function's behavior. Every decorator contains a wrapper.

🎯 What's Next?

← All How-To Guides