Name reusable behaviour; wrap with cross-cutting concerns; pass a small inline callback.
A function is a recipe card; decorator is a laminator that adds ‘gluten-free’ or ‘spicy’ to the card; lambda is a sticky note for a one-liner.
@trace
def add(a,b): return a+b
becomes: add = trace(add)
call add(2,3) -> wrapper prints CALL -> add(2,3) -> 5
Functions are first-class: assignable, passable, returnable. Decorators wrap a function (stacking @a @b def f applies bottom-up). @functools.wraps(func) preserves __name__/__doc__ - always use it. Lambdas are restricted to one expression for throwaway callbacks.
def greet(name):
return f'Hello, {name}'
print(greet('Ada'))
simplest def; positional arg.
import functools
def trace(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'CALL {func.__name__}')
return func(*args, **kwargs)
return wrapper
@trace
def add(a, b): return a + b
decorator with *args, **kwargs; @functools.wraps keeps metadata.
tickets = [('hi',1),('low',5),('med',3)]
print(sorted(tickets, key=lambda t: t[1]))
lambda as standard key= callback.
Writing a decorator without @functools.wraps(func) - the wrapped function loses its __name__, breaking debuggers, Sentry fingerprinting, OpenAPI schemas. Always @wraps(func).
Default to def; lambdas only for key=/value= callbacks; @functools.wraps on every decorator; prefer *args, **kwargs forwarding.
@functools.wraps.from functools import wraps and @wraps(func) everywhere.def wrapper without wr; lint rule B901.assert cb(i) == i.i by NAME, all closures saw loop’s final i.lambda i=i: do(i); or factory function.