functools
functools is a standard library module full of higher-order functions — functions that operate on other functions. Several of its tools (reduce, partial, wraps) are covered in more depth on their own dedicated pages earlier in this course; here we'll recap those briefly and then go deeper on the tools that don't have their own page yet: lru_cache, cmp_to_key, and singledispatch.
`lru_cache`: Memoizing Function Results
functools.lru_cache is a decorator that caches a function's return values, keyed by the arguments it was called with. If the function is called again with the same arguments, the cached result is returned instantly instead of recomputing it. "LRU" stands for "Least Recently Used" — when a maximum cache size is set and the cache fills up, the least recently used entries are discarded first.
The classic example is a naive recursive Fibonacci function, which recalculates the same values over and over as the input grows:
import time
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
start = time.perf_counter()
result = fib(30)
elapsed = time.perf_counter() - start
print(f"fib(30) = {result}, took {elapsed:.3f}s")fib(30) = 832040, took 0.285s
Now add @lru_cache(maxsize=None) above the function — nothing else changes — and repeated sub-calls (like fib(28) being needed by both fib(29) and fib(30)) are served from the cache instead of being recomputed:
import time
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_cached(n):
if n < 2:
return n
return fib_cached(n - 1) + fib_cached(n - 2)
start = time.perf_counter()
result = fib_cached(30)
elapsed = time.perf_counter() - start
print(f"fib_cached(30) = {result}, took {elapsed:.6f}s")
# A second call with the same argument is served straight from the cache.
start = time.perf_counter()
fib_cached(30)
elapsed = time.perf_counter() - start
print(f"second call took {elapsed:.6f}s (cache hit)")fib_cached(30) = 832040, took 0.000041s second call took 0.000001s (cache hit)
The timings above are illustrative of the effect, but the pattern holds: uncached recursive Fibonacci grows exponentially slower as n increases, while the cached version is effectively linear because each unique n is only ever computed once.
You can also cap how many results are remembered with maxsize (e.g. @lru_cache(maxsize=128)), and inspect cache effectiveness at any time with fib_cached.cache_info(), which reports hits, misses, and current size.
`reduce()`: Folding a Sequence Down to One Value
functools.reduce() repeatedly applies a two-argument function to the items of a sequence, carrying an accumulated result forward, until a single value remains. It's covered in much more depth on the map/filter/reduce page of this course — here's a quick recap:
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, n: acc + n, numbers)
product = reduce(lambda acc, n: acc * n, numbers)
print("sum:", total)
print("product:", product)sum: 15 product: 120
`partial()`: Pre-Filling Arguments
functools.partial() creates a new, specialized version of a function with some arguments already filled in. This is covered in depth on the partial-functions page of this course; the short version:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5))
print(cube(2))25 8
`wraps()`: Preserving Metadata in Decorators
Decorators are covered fully on the decorators page of this course. As a quick recap of why functools.wraps matters: when a decorator replaces a function with a wrapper, the wrapper normally "hides" the original function's __name__ and __doc__. wraps copies that metadata across so introspection tools (and other developers) still see the original function's identity.
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name):
"""Return a friendly greeting."""
return f"Hello, {name}!"
print(greet("Ada"))
print(greet.__name__)
print(greet.__doc__)Calling greet Hello, Ada! greet Return a friendly greeting.
Without @wraps(func), greet.__name__ would print "wrapper" and greet.__doc__ would be None instead of the original docstring.
`cmp_to_key()`: Adapting Old-Style Comparators
sorted() and list.sort() expect a key= function that maps each item to a value to sort by. Older code (and some algorithms that are naturally expressed as comparisons) uses a different style: a two-argument comparison function that returns a negative number, zero, or a positive number depending on whether the first argument should sort before, equal to, or after the second. functools.cmp_to_key() adapts a comparator like that into a key= function.
from functools import cmp_to_key
def compare_lengths(a, b):
if len(a) < len(b):
return -1
if len(a) > len(b):
return 1
return 0
words = ["banana", "kiwi", "fig", "watermelon"]
words.sort(key=cmp_to_key(compare_lengths))
print(words)['fig', 'kiwi', 'banana', 'watermelon']
`singledispatch`: Function Overloading by Type
Python doesn't have built-in function overloading the way some other languages do — you can't define multiple versions of a function that differ only by parameter type. functools.singledispatch gets you something close: it lets you register different implementations of a function based on the type of its first argument, while callers keep calling a single function name.
from functools import singledispatch
@singledispatch
def describe(value):
print(f"Something else: {value!r}")
@describe.register
def _(value: int):
print(f"An integer: {value} (double is {value * 2})")
@describe.register
def _(value: str):
print(f"A string of length {len(value)}: {value!r}")
@describe.register
def _(value: list):
print(f"A list with {len(value)} items: {value!r}")
describe(42)
describe("hello")
describe([1, 2, 3])
describe(3.14)An integer: 42 (double is 84) A string of length 5: 'hello' A list with 3 items: [1, 2, 3] Something else: 3.14
The @describe.register decorator uses the type hint on the single parameter to know which type that implementation handles. Calling describe(3.14) falls back to the original undecorated function, since no implementation was registered for float. This pattern is especially useful for things like serializers or renderers that need to behave differently depending on the shape of the data they're given, without a long chain of isinstance() checks.
Quick Reference
Tool | Purpose | Covered in depth on |
|---|---|---|
| Memoize results for repeated calls with the same arguments | This page |
| Fold a sequence down to a single accumulated value | The map/filter/reduce page |
| Pre-fill some arguments to create a specialized function | The partial-functions page |
| Preserve | The decorators page |
| Adapt an old two-argument comparator into a | This page |
| Overload a function based on its first argument’s type | This page |
lru_cachetrades memory for speed — great for pure functions with a limited, repeating set of inputs, especially recursive ones.reduce,partial, andwrapsare everyday tools with their own dedicated pages in this course;functoolsbundles them alongside the more advanced tools above.singledispatchonly dispatches on the type of the first argument — for multiple-argument dispatch, look at the relatedsingledispatchmethodor third-party multiple-dispatch libraries.