#1 Data Analytics Program in India
₹2,499₹1,499Enroll Now
8 min read
•Question 15 of 41medium

Decorators in Python

Understanding Python decorators.

What You'll Learn

  • What decorators are and how they work
  • Writing basic and parameterized decorators
  • Using functools.wraps to preserve metadata
  • Class decorators and decorator stacking
  • Common real-world decorator patterns

Understanding Decorators

Decorators are functions that modify the behavior of other functions without changing their code. They're a powerful way to add functionality like logging, authentication, caching, and more.

The @decorator syntax is just syntactic sugar for:

code.pyPython
@decorator
def func():
    pass

# Is equivalent to:
func = decorator(func)

How Decorators Work

code.pyPython
# Step 1: A decorator is a function that takes a function
def my_decorator(func):
    # Step 2: It defines a wrapper function
    def wrapper(*args, **kwargs):
        print("Before the function call")
        result = func(*args, **kwargs)  # Call original function
        print("After the function call")
        return result
    # Step 3: It returns the wrapper
    return wrapper

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

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

Basic Decorator Pattern

code.pyPython
from functools import wraps

def logger(func):
    @wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        print(f"Calling: {func.__name__}")
        print(f"Args: {args}, Kwargs: {kwargs}")
        result = func(*args, **kwargs)
        print(f"Returned: {result}")
        return result
    return wrapper

@logger
def add(a, b):
    """Add two numbers."""
    return a + b

add(2, 3)
# Calling: add
# Args: (2, 3), Kwargs: {}
# Returned: 5

Why Use @wraps?

Without @wraps, the decorated function loses its metadata:

code.pyPython
from functools import wraps

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

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

@bad_decorator
def hello():
    """Says hello."""
    pass

print(hello.__name__)  # "wrapper" (wrong!)
print(hello.__doc__)   # None (lost!)

@good_decorator
def hello2():
    """Says hello."""
    pass

print(hello2.__name__)  # "hello2" (correct!)
print(hello2.__doc__)   # "Says hello." (preserved!)

Decorators with Arguments

code.pyPython
from functools import wraps

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(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Multiple Decorators (Stacking)

Decorators are applied bottom-up:

code.pyPython
@decorator1
@decorator2
@decorator3
def func():
    pass

# Equivalent to:
func = decorator1(decorator2(decorator3(func)))

Common Real-World Decorators

Timer Decorator

code.pyPython
import time
from functools import wraps

def timer(func):
    @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} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

Retry Decorator

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

@retry(max_attempts=3)
def unstable_api_call():
    # May fail sometimes
    pass

Memoization/Caching

code.pyPython
from functools import lru_cache

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

fibonacci(100)  # Computed once, cached for future calls

Authentication Decorator

code.pyPython
def require_auth(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.is_authenticated:
            raise PermissionError("Authentication required")
        return func(user, *args, **kwargs)
    return wrapper

@require_auth
def view_dashboard(user):
    return "Dashboard data"

Class Decorators

Decorate classes to modify their behavior:

code.pyPython
def singleton(cls):
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    pass

db1 = Database()
db2 = Database()
print(db1 is db2)  # True - same instance

Interview Tip

When asked about decorators:

  1. They're functions that wrap other functions
  2. The @decorator syntax is syntactic sugar
  3. Always use @wraps to preserve metadata
  4. Know the pattern for parameterized decorators (triple-nested)
  5. Common uses: logging, timing, caching, auth, retry
  6. Apply bottom-up when stacking multiple decorators