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 accessThe 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.