Python functools, explained
functools is the standard library's toolbox for higher-order functions — utilities
that take or return functions. A handful of its tools show up constantly in real code and
interviews: lru_cache, partial, reduce, wraps, and cached_property. Here's what
each does and when to reach for it.
lru_cache — memoisation for free
Decorating a function with lru_cache stores results keyed by arguments, so repeated calls
with the same inputs return instantly. It turns exponential recursion into linear time.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(100) # fast — each subproblem computed once
fib.cache_info() # CacheInfo(hits=..., misses=101, maxsize=None, currsize=101)
maxsize bounds the cache (least-recently-used items evicted). Arguments must be
hashable, and the cached function should be pure — caching a function with side
effects or that depends on external state will give stale results. Python 3.9+ adds
@cache as shorthand for lru_cache(maxsize=None).
partial — pre-fill arguments
partial builds a new callable with some arguments already supplied. It's a clean
alternative to lambdas for "the same function but with this argument fixed."
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
square(5) # 25
cube(5) # 125
Common use: adapting a function to a callback API that passes fewer arguments, or building
specialised versions like int_base2 = partial(int, base=2).
reduce — fold a sequence to one value
reduce repeatedly applies a two-argument function across an iterable, accumulating a single
result. It's the general form behind sum, max, and friends.
from functools import reduce
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4], 1) # 24
The third argument is the initial value (and the result for an empty iterable). Prefer a
plain loop or built-in when one exists — reduce is most justified for genuine folds like
running a chain of compositions.
wraps — keep the wrapped function's identity
When you write a decorator, the wrapper replaces the original, losing its __name__ and
docstring. functools.wraps copies that metadata back.
from functools import wraps
def log(func):
@wraps(func) # without this, greet.__name__ == 'wrapper'
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log
def greet(): "say hi"
greet.__name__ # 'greet'
Always apply @wraps in your decorators — it keeps introspection, debuggers, and docs
working.
cached_property — compute once per instance
cached_property turns an expensive method into an attribute computed on first access and
then stored on the instance, so later accesses are free.
from functools import cached_property
class Dataset:
def __init__(self, rows):
self.rows = rows
@cached_property
def stats(self):
print("computing...")
return {"count": len(self.rows), "total": sum(self.rows)}
d = Dataset([1, 2, 3])
d.stats # "computing..." then the dict
d.stats # cached — no recompute
Because the result is stored in the instance __dict__, it lives as long as the instance.
Use it for derived values that are costly and don't change.
reduce vs comprehensions, and other tools
functools also offers singledispatch (function overloading by argument type),
total_ordering (fill in comparison methods from __eq__ and one of __lt__ etc.), and
cmp_to_key (adapt old-style comparison functions for sorted). These are niche but handy
when they fit.
Recap
functools packages the most useful higher-order helpers: lru_cache/cache memoise
pure, hashable-argument functions; partial pre-fills arguments to make specialised
callables; reduce folds an iterable to a single value; wraps preserves a wrapped
function's name and docstring inside decorators; and cached_property computes an
expensive attribute once per instance. Round it out with singledispatch and
total_ordering when you need type-based dispatch or auto-generated comparisons.