Python functools Module
debt(d6/e3/b3/t5)
Closest to 'only careful code review or runtime testing' (d7), but nudged to d6 because flake8-functions and a custom code_pattern regex can catch the missing-wraps case, while lru_cache misuse (unhashable args, memory leaks) stays silent until runtime. Specialist plugins catch some cases but not the subtle memoization bugs.
Closest to 'simple parameterised fix' (e3). quick_fix is adding @functools.wraps(func) or swapping a lambda for partial — a small pattern replacement, occasionally requiring adding maxsize= to lru_cache, still localised to the decorator/cache definition.
Closest to 'localised tax' (b3). Although applies_to spans web/cli/library/queue, each functools use (a decorator, a partial, a cache) is a localised choice; missing wraps only taxes introspection/framework routing around that decorator rather than shaping the whole system.
Closest to 'notable trap' (t5). The misconception (functools is a niche grab-bag) plus documented gotchas — decorators silently losing __name__ without wraps, lru_cache caching stale/leaking results — are traps most devs eventually learn but the naive path gets wrong.
Also Known As
TL;DR
Explanation
functools is Python's standard-library toolkit for working with callables as first-class values. Its most important members solve real problems that arise when you wrap, adapt, or memoize functions.
functools.wraps is the single most important tool for anyone writing decorators. A naive decorator replaces the wrapped function with an inner closure, silently destroying __name__, __doc__, __module__, and __wrapped__. Applying @functools.wraps(func) to the inner function copies that metadata across, so introspection, help(), and debuggers still see the original function.
functools.partial performs partial application: it binds some arguments now and returns a new callable that takes the rest later. This is cleaner than a lambda for pre-filling configuration, callbacks, or dependency arguments, and unlike a lambda it is picklable and carries .func, .args, and .keywords attributes.
functools.lru_cache (and the simpler cache in 3.9+) memoizes a pure function's return value keyed on its arguments, turning repeated expensive calls into dictionary lookups. Arguments must be hashable, and you should never cache functions with side effects or unbounded key spaces without a maxsize.
functools.reduce folds an iterable into a single value with a binary function; often a comprehension or sum() reads better, but reduce is right for genuine accumulations. functools.singledispatch turns a function into a type-dispatched generic, registering separate implementations per argument type - a clean alternative to isinstance chains. cached_property memoizes an instance attribute computed once per object.
The theme across all of these is reuse of function behaviour without rewriting it: wrap it (wraps), pre-configure it (partial), remember its results (lru_cache), or specialize it by type (singledispatch). Reaching for functools first keeps decorator and adapter code correct and idiomatic instead of hand-rolled and buggy.
Common Misconception
Why It Matters
Common Mistakes
- Writing a decorator without @functools.wraps, so the wrapped function loses its name, docstring, and signature.
- Using lru_cache on a method or function with side effects or unhashable arguments, causing stale results or TypeError.
- Applying lru_cache without a maxsize on unbounded input, leaking memory as the cache grows forever.
- Reaching for functools.reduce where a plain sum(), any(), or comprehension would be clearer and faster.
- Using a lambda to pre-fill arguments when functools.partial is picklable and more introspectable.
Avoid When
- Caching a function with side effects or non-deterministic output with lru_cache, where stale results cause bugs.
- Using reduce when a comprehension, sum(), any(), or all() expresses the intent more readably.
- Applying lru_cache to instance methods that hold references, keeping objects alive and leaking memory.
When To Use
- Writing any decorator - always wrap the inner function with functools.wraps.
- Pre-binding configuration or callback arguments where partial is clearer and picklable versus a lambda.
- Memoizing pure, expensive, deterministic functions with hashable arguments via lru_cache or cache.
- Replacing isinstance dispatch chains with functools.singledispatch for type-based generic functions.
Code Examples
import time
# Decorator loses metadata; caching hand-rolled and unbounded
def timed(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(func.__name__, time.perf_counter() - start)
return result
return wrapper
_cache = {}
def fib(n):
if n in _cache:
return _cache[n]
_cache[n] = n if n < 2 else fib(n - 1) + fib(n - 2)
return _cache[n]
@timed
def greet(name):
"""Say hello."""
return f'Hello {name}'
print(greet.__name__) # 'wrapper' - metadata lost
print(greet.__doc__) # None
import functools
import time
def timed(func):
@functools.wraps(func) # preserves __name__, __doc__, __wrapped__
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(func.__name__, time.perf_counter() - start)
return result
return wrapper
@functools.lru_cache(maxsize=None) # memoized, no manual dict
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
@timed
def greet(name: str) -> str:
"""Say hello."""
return f'Hello {name}'
# partial application instead of a lambda
log_error = functools.partial(print, '[ERROR]')
print(greet.__name__) # 'greet' - metadata preserved
print(greet.__doc__) # 'Say hello.'
log_error('disk full')