functools
functools supplies higher-order functions for caching, partial application, reduction, and single-dispatch generics - common building blocks in decorators and APIs.
Recipe
from functools import lru_cache, partial
@lru_cache(maxsize=128)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
double = partial(pow, 2)When to reach for this:
- Memoizing expensive pure functions
- Currying configuration into callbacks
- Writing decorators that preserve
__name__ - Type-specific function behavior without
if isinstancechains
Working Example
from functools import lru_cache, partial, reduce, singledispatch, wraps
def logger(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(f"call {fn.__name__}")
return fn(*args, **kwargs)
return wrapper
@logger
@lru_cache(maxsize=32)
def tokenize(text: str) -> tuple[str, ...]:
return tuple(text.lower().split())
@singledispatch
def serialize(value) -> str:
return str(value)
@serialize.register(int)
def _(value: int) -> str:
return f"i:{value}"
def product(nums: list[int]) -> int:
return reduce(lambda a, b: a * b, nums, 1)
if __name__ == "__main__":
print(tokenize("Hello World"))
print(serialize(42))
print(product([2, 3, 4]))
add5 = partial(lambda a, b: a + b, 5)
print(add5(10))What this demonstrates:
@wrapscopies metadata to wrapper for introspection/debugginglru_cacherequires hashable argumentssingledispatchregisters per-type handlersreducefolds iterable with binary function
Deep Dive
How It Works
- lru_cache - Dict keyed by arguments; evicts least recently used at maxsize.
- cache_clear - Invalidates memoized results when inputs change externally.
- partial -
partial(func, *args, **kwargs)fixes leading arguments. - singledispatch - Dispatches on type of first argument; register with decorator.
- total_ordering - Generates comparison methods from
__eq__and one ordering.
Decorator Stack Order
@outer
@inner
def f(): ...
# equals f = outer(inner(f)) - inner applied firstPython Notes
from functools import cached_property # 3.8+
class Config:
@cached_property
def heavy(self) -> dict:
return load()Gotchas
- lru_cache on mutable args - Lists unhashable. Fix: Tuple arguments or disable cache.
- Caching impure functions - Stale reads when hidden state changes. Fix: cache_clear or no cache.
- Unbounded cache maxsize=None - Memory growth on infinite key space. Fix: Set maxsize.
- partial obscures signature - inspect less clear. Fix: wrapper with explicit params for public APIs.
- singledispatch not for multiple args - Only first arg type. Fix: multimethod libraries or manual match.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| manual dict cache | Custom eviction | lru_cache suffices |
@cache (3.9+) | Unbounded small key space | Need LRU eviction |
| match/case dispatch | Few types inline | Open extension across modules |
itertools.accumulate | Running fold without reduce import | Need final single value only |
FAQs
lru_cache vs cache?
functools.cache unbounded LRU-like simple cache (3.9+). lru_cache controls size and stats.
thread-safe lru_cache?
CPython protects cache dict for single operations - still coordinate if underlying function not thread-safe.
when partial?
Bind config callbacks - partial(send_email, smtp=cfg) in loops.
wraps necessary?
Yes for public decorators - preserves __name__, __doc__, annotations for tooling.
reduce still pythonic?
Acceptable for functional folds; often clearer total = 1; for x in nums: total *= x for beginners.
singledispatch extension?
Register in other modules with same function name imported - order matters.
cached_property vs lru_cache?
cached_property stores on instance once; lru_cache on function arguments globally.
total_ordering caveat?
Must define __eq__ and one comparison; others derived - can be slower than manual.
cache_info?
fib.cache_info() hits/misses/maxsize/currsize for tuning.
functools in hot path?
lru_cache helps expensive pure calls; partial negligible cost.
Related
- Writing Decorators - wraps pattern
- Closures & Scope Capture - factory pattern
- Overloads & Callable Types - typing overloads
- itertools - accumulate vs reduce
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.