Decorators
In Python, functions are objects — you can pass them around, store them in variables, and return them from other functions. A function that takes or returns a function is a higher-order function, and a decorator is exactly that: a function that takes a function, wraps it in extra behaviour, and gives back the wrapped version.
The @decorator line above a def is just sugar. @shout over def greet(): ... means greet = shout(greet) — the name now points at the wrapper, which calls the original inside itself. That lets you bolt on timing, logging, retries, or caching across a whole codebase without editing each function.
Always wrap the inner function with functools.wraps(fn). Without it, the wrapper steals the original's identity — greet.__name__ becomes "wrapper" and the docstring vanishes, which breaks debugging and documentation tools. wraps copies the metadata across so the decorated function still looks like itself.
For a decorator that takes arguments (like @retry(3)) you need one more layer: an outer factory that takes the args and returns the actual decorator. So it's three nested functions deep — factory → decorator → wrapper. A classic use is memoization: cache a function's results by its arguments so repeat calls are instant.
Syntax
import functools
def my_decorator(fn):
@functools.wraps(fn) # preserve fn's name/docstring
def wrapper(*args, **kwargs):
# ... before ...
result = fn(*args, **kwargs)
# ... after ...
return result
return wrapper
@my_decorator # same as: greet = my_decorator(greet)
def greet(name): ...
# Decorator WITH arguments — one extra layer:
def retry(times):
def decorator(fn): ...
return decoratorWorked examples
A wrapping decorator, with functools.wraps keeping identity
import functools
def shout(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"Return a greeting."
return f"hi {name}"
print(greet("sam")) # HI SAM
print(greet.__name__) # greet (preserved by wraps)
# Output:
# HI SAM
# greetMemoize — cache results by arguments
import functools
def memoize(fn):
cache = {}
@functools.wraps(fn)
def wrapper(*args):
if args not in cache:
cache[args] = fn(*args)
return cache[args]
return wrapper
calls = []
@memoize
def square(n):
calls.append(n) # records each REAL computation
return n * n
print(square(5), square(5)) # 25 25
print("real calls:", len(calls)) # 1 — second was cached
# Output:
# 25 25
# real calls: 1A decorator with an argument (retry factory)
import functools
def retry(times):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs)
except Exception:
if attempt == times - 1:
raise # give up on the last try
return wrapper
return decorator
log = []
@retry(3)
def flaky():
log.append(1)
if len(log) < 3:
raise ValueError("not yet")
return "ok"
print(flaky()) # ok
print(len(log)) # 3 (failed twice, succeeded on the third)
# Output:
# ok
# 3Common mistakes & gotchas
Forgetting functools.wraps
Without @functools.wraps(fn) on the inner wrapper, the decorated function reports __name__ as "wrapper" and loses its docstring. Tracebacks, help(), and documentation generators all get confused. It's a one-line habit — always add it.
A decorator-with-args needs THREE layers
@retry(3) first *calls* retry(3), which must return a decorator, which then receives the function. Miss a layer and you get TypeError: decorator() takes 1 positional argument but .... The shape is factory → decorator → wrapper; the factory returns decorator, not wrapper.
Mutable default caches are shared
Defining the cache = {} inside the decorator (one per decorated function) is correct. Putting a mutable cache as a *default argument* (def wrapper(args, cache={})) creates one shared dict across unrelated calls — a classic mutable-default-argument bug. Keep state in the closure, as the memoize example does.
Why it matters
Decorators are everywhere in real frameworks — `@app.route` in Flask, `@pytest.fixture`, `@property`, `@functools.lru_cache`. They're the mechanism for cross-cutting concerns: the stuff (auth, timing, caching, logging) you want around many functions without copy-pasting it into each one. Reading and writing them is a core senior-level skill.