Tutorials Logic, IN +91 8092939553 info@tutorialslogic.com
FAQs Support
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Interview Questions Website Development
Compiler Tutorials

Decorators in Python

What is a 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.

How Decorators Work

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)

Preserving Function Metadata

functools.wraps
from functools import wraps

def my_decorator(func):
    @wraps(func)   # preserves __name__, __doc__, etc.
    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__)  # greet  (not 'wrapper')
print(greet.__doc__)   # Greet someone by name.

Practical Decorator Examples

Practical 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
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

Ready to Level Up Your Skills?

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