Hiprup

What are Python decorators and how do they work?

A decorator is a function that wraps another function to extend or modify its behavior — without changing its source code. It's syntactic sugar for the higher-order function pattern.

How they Works
Under the hood, when you write @my_decorator above a function, Python is essentially doing func = my_decorator(func) — it passes the original function into the decorator and replaces it with whatever the decorator returns, which is typically a wrapper function.

Mental model — the sugar:

@timer
def work(): ...

# is exactly equivalent to:
def work(): ...
work = timer(work)

Basic decorator:

import functools, time

def timer(fn):
    @functools.wraps(fn)               # preserves name, docstring, signature
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} took {time.perf_counter() - t0:.4f}s")
        return result
    return wrapper

@timer
def heavy(n): return sum(range(n))

Decorators with arguments — add one more layer of nesting (a decorator factory):

def retry(times=3):
    def deco(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for i in range(times):
                try: return fn(*args, **kwargs)
                except Exception:
                    if i == times - 1: raise
        return wrapper
    return deco

@retry(times=5)
def flaky(): ...

import functools
import time

# Basic decorator
def timer(func):
    @functools.wraps(func)  # Preserve original function metadata
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f'{func.__name__} took {elapsed:.4f}s')
        return result
    return wrapper

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

slow_function()  # "slow_function took 1.0012s"

# Decorator with arguments
def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.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
                    print(f'Attempt {attempt + 1} failed: {e}. Retrying...')
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def fetch_data(url):
    # Might fail due to network issues
    return requests.get(url).json()

# Built-in decorators
from functools import lru_cache

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

# Class-based decorators
@staticmethod    # No self parameter
@classmethod     # cls instead of self
@property        # Getter as attribute access

The timer decorator wraps any function with timing logic. @functools.wraps preserves the original function's name and doc. The retry decorator takes parameters — it is a decorator factory (function that returns a decorator). @lru_cache is a built-in decorator that memoizes function results. *args and **kwargs make decorators work with any function signature.

Show a practical decorator (timer or retry), explain how it works (@syntax is sugar for function reassignment), and always use functools.wraps. The decorator-with-arguments pattern (triple nesting) is the advanced follow-up.

Mention @lru_cache, @property, @staticmethod as built-in examples.

What are Python decorators and how do they work? | Hiprup