Home Tutorials Standard Library

Standard Library

functools Essentials: reduce(), partial(), lru_cache, and wraps

Pyford Notes July 6, 2026 7 min read
Key points
  • reduce() folds a sequence down to a single value by repeatedly applying a two-argument function.
  • partial() creates a new function with some arguments already filled in, useful for adapting callables to interfaces they don't quite match.
  • @lru_cache memoises a function's results automatically, based on its argument values.
  • @wraps preserves a decorated function's name, docstring, and metadata, which most hand-written decorators silently break without it.

reduce(): folding a sequence into one value

functools.reduce() applies a function of two arguments cumulatively to the items of a sequence, reducing it to a single value:

from functools import reduce

numbers = [1, 2, 3, 4]
total = reduce(lambda acc, x: acc + x, numbers)
print(total)   # 10

Each step passes the running result (acc) and the next item (x) to the function. For summing, sum() is clearer and should be preferred, but reduce() generalises to any combining operation — finding the maximum, building up a running product, or merging a list of dictionaries into one:

dicts = [{"a": 1}, {"b": 2}, {"c": 3}]
merged = reduce(lambda acc, d: {**acc, **d}, dicts, {})
print(merged)   # {'a': 1, 'b': 2, 'c': 3}

The third argument, {}, is the initial value the accumulator starts from — useful both to handle an empty input sequence gracefully and to make the starting state explicit rather than implicit.

partial(): pre-filling arguments

functools.partial() takes a function and some arguments, and returns a new callable that has those arguments already bound:

from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5))   # 25
print(cube(5))     # 125

This is especially handy when an existing function's signature does not quite match what a callback-based API expects — rather than writing a small wrapper function by hand for every case, partial() builds one from the original function and the arguments you already know.

lru_cache: memoising expensive calls

Memoisation means storing the result of a function call so a repeated call with the same arguments returns the cached answer instead of recomputing it. @lru_cache adds this automatically:

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(30))   # fast, even though naive recursion is exponential

Without the cache, naive recursive Fibonacci recomputes the same values an exponential number of times. With @lru_cache, each distinct argument value is computed once and then served from the cache on every later call, turning an exponential-time function into a linear-time one for practical purposes. "LRU" stands for least-recently-used — when maxsize is a number rather than None, the least recently used entries are dropped once the cache is full.

Inspecting a cache with cache_info()

A cached function gains a cache_info() method that reports how well the cache is performing:

print(fibonacci.cache_info())
# CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)

hits counts calls answered from the cache, misses counts calls that had to compute a fresh result, and currsize shows how many distinct argument combinations are currently stored. fibonacci.cache_clear() empties the cache if you need to reset it, which is occasionally useful in long-running programs or tests.

wraps: keeping a decorated function's identity

A hand-written decorator that wraps a function in another function normally hides the original function's name and docstring behind the wrapper's:

def logged(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@logged
def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}"

print(greet.__name__)   # 'wrapper' -- not 'greet'!
print(greet.__doc__)    # None -- the docstring is gone

functools.wraps fixes this by copying the original function's metadata onto the wrapper:

from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@logged
def greet(name):
    """Say hello to someone."""
    return f"Hello, {name}"

print(greet.__name__)   # 'greet'
print(greet.__doc__)    # 'Say hello to someone.'

Without @wraps, tools that rely on function names and docstrings — documentation generators, debuggers, introspection-based frameworks — see the generic wrapper instead of the real function, which makes decorated code noticeably harder to debug in a larger codebase.

total_ordering: filling in comparisons

functools.total_ordering fills in the remaining rich comparison methods once you define __eq__ and just one of __lt__, __le__, __gt__, or __ge__:

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        return self.number == other.number

    def __lt__(self, other):
        return self.number < other.number

v1, v2 = Version(1), Version(2)
print(v1 < v2, v1 <= v2, v1 > v2, v1 >= v2)   # True True False False

Without the decorator, you would need to write all four comparison methods by hand even though three of them are mechanically derivable from the other two, making total_ordering a small but genuine reduction in repetitive, error-prone code.