Python functools Module

The functools module is part of Python’s standard library and provides tools for working with functions as objects: caching their results, pre-filling their arguments, combining values across a sequence, preserving metadata through decorators, and dispatching behavior by type. If you have ever written a recursive function that recomputes the same values over and over, or a decorator that mysteriously breaks help(), functools is the module that solves those problems in a few lines instead of a lot of custom code.

Overview: What functools Does and Why It Exists

In Python, functions are first-class objects: they can be stored in variables, passed as arguments, returned from other functions, and have attributes attached to them (__name__, __doc__, __module__, and so on). functools exists to exploit that fact. Instead of writing boilerplate for common patterns — memoizing expensive calls, building specialized versions of a general function, reducing a sequence to one value, preserving a wrapped function’s identity, or comparing custom objects — you import a ready-made, well-tested implementation.

The most commonly used members are:

  • functools.reduce(function, iterable[, initializer]) — repeatedly applies a two-argument function to the items of an iterable, reducing it to a single cumulative value.
  • functools.lru_cache(maxsize=128, typed=False) / functools.cache — wraps a function so its return values are memoized (cached) by argument values, avoiding repeated expensive work.
  • functools.partial(func, *args, **kwargs) — returns a new callable with some arguments already "frozen in", useful for specializing general-purpose functions.
  • functools.wraps(wrapped) — a decorator-writer’s decorator that copies a wrapped function’s __name__, __doc__, and other metadata onto a wrapper function.
  • functools.singledispatch — turns a plain function into a generic function that dispatches to different implementations depending on the type of its first argument.
  • functools.total_ordering — a class decorator that fills in missing rich comparison methods (<, <=, >, >=) given just __eq__ and one of them.
  • functools.cached_property — a descriptor that computes an instance attribute once and caches it on the instance.
  • functools.cmp_to_key — converts an old-style comparison function (returning negative/zero/positive) into a key function usable with sorted().

Internally, most of these are thin, carefully-written wrappers. lru_cache, for example, stores results in a dictionary keyed by a hashable representation of the call’s arguments, and — when a maxsize is set — maintains an internal doubly linked list to track which entries were least recently used so it can evict them once the cache is full. partial does not generate new bytecode; it stores the original function together with the given positional and keyword arguments in a small object, and calling that object merges the stored arguments with whatever you pass at call time before invoking the original function. Because these tools operate on the function objects themselves, they compose naturally with decorators, closures, and classes.

Syntax

Function Signature Purpose
reduce reduce(function, iterable, initializer=None) Cumulatively combine items into one value
lru_cache @lru_cache(maxsize=128, typed=False) Memoize return values by argument
cache @cache Unbounded memoization (Python 3.9+, shorthand for lru_cache(maxsize=None))
partial partial(func, *args, **kwargs) Pre-bind some arguments of a function
wraps @wraps(original_func) Copy metadata from the original function onto a wrapper
singledispatch @singledispatch then @func.register(Type) Dispatch by the type of the first argument
total_ordering @total_ordering on a class Derive missing comparison operators
cached_property @cached_property on a method Compute once, cache on the instance

Every entry above is used as a decorator (placed with @ directly above a def) except reduce and partial, which are called directly to produce a value or a new callable.

Examples

Example 1: reduce for cumulative computation

from functools import reduce
import operator

numbers = [1, 2, 3, 4, 5]

total = reduce(lambda acc, x: acc + x, numbers)
product = reduce(operator.mul, numbers, 1)

print(f"Sum: {total}")
print(f"Product: {product}")

Output:

Sum: 15
Product: 120

reduce takes the first two items, applies the function, then combines the running result with each subsequent item. For the sum, 1+2=3, then 3+3=6, then 6+4=10, then 10+5=15. The initializer argument (here 1 for the product) is used as the starting accumulator instead of the first element, which matters when the iterable could be empty or when the identity element differs from the first item.

Example 2: lru_cache for memoized recursion

from functools import lru_cache

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

print(fibonacci(30))
print(fibonacci.cache_info())

fibonacci.cache_clear()
print(fibonacci.cache_info())

Output:

832040
CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)
CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)

Without caching, naive recursive Fibonacci recomputes the same sub-values exponentially many times. With lru_cache, each distinct argument value (n from 0 to 30) is computed only once — that is the 31 misses — and any later call with an argument already seen returns instantly from the dictionary-backed cache — the 28 hits. cache_info() reports these counters so you can verify the cache is actually helping. cache_clear() empties the cache, resetting all counters to zero.

Example 3: partial for specializing functions

from functools import partial

def power(base: float, exponent: float) -> float:
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))
print(cube(2))
print(square(base=10))

Output:

25
8
100

partial(power, exponent=2) returns a new callable that already has exponent fixed at 2; calling square(5) is equivalent to power(5, exponent=2). This is useful anywhere you would otherwise write a small wrapper function just to fill in one or two arguments — for example, preparing callbacks for GUI event handlers, map() calls, or configuring a logging function with a fixed prefix.

Example 4: wraps for well-behaved decorators

from functools import wraps

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

@logged
def greet(name: str) -> str:
    """Return a greeting for name."""
    return f"Hello, {name}!"

print(greet("Ada"))
print(greet.__name__)
print(greet.__doc__)

Output:

Calling greet(('Ada',), {})
Hello, Ada!
greet
Return a greeting for name.

Without @wraps(func), greet.__name__ would be "wrapper" and greet.__doc__ would be None — because after decoration, greet literally is the inner wrapper function. wraps copies over __name__, __doc__, __module__, __qualname__, and updates __dict__, and it also sets __wrapped__ to point back at the original function so introspection tools and help() keep working correctly.

Under the Hood: How These Tools Actually Work

reduce is a simple loop in disguise: it maintains an accumulator variable, iterates the sequence once, and repeatedly reassigns the accumulator to function(accumulator, next_item). There is no magic — you could write the loop yourself, but reduce documents the intent clearly and is implemented in C in CPython for speed.

lru_cache builds a dictionary whose keys are derived from the positional and keyword arguments passed to the decorated function. This means every argument must be hashable — lists, dicts, and sets cannot be used as arguments to a cached function. When maxsize is a number rather than None, the cache also maintains a doubly linked list recording usage order; each cache hit moves that entry to the "most recently used" end, and once the dictionary would exceed maxsize entries, the least-recently-used entry is evicted from both the dictionary and the linked list. Setting maxsize=None (or using @cache, added in Python 3.9) disables eviction entirely, so the cache grows without bound for the life of the process.

partial objects store a reference to the original function plus the frozen positional and keyword arguments. When called, they concatenate the stored positional arguments with the new ones, merge the keyword dictionaries (new keywords win over stored ones only if you explicitly override them), and call the underlying function. No new function is compiled — it is just argument bookkeeping around the original callable.

Common Mistakes

Mistake 1: Caching a function called with unhashable arguments

Applying lru_cache to a function that is sometimes called with a list argument fails, because lru_cache needs to hash the arguments to use them as a dictionary key:

process([1, 2, 3]) raises TypeError: unhashable type: 'list' if process is decorated with @lru_cache and takes a list.

The fix is to accept a hashable collection instead, such as a tuple:

from functools import lru_cache

@lru_cache(maxsize=None)
def process(values: tuple[int, ...]) -> int:
    return sum(v * v for v in values)

result = process((1, 2, 3))
print(result)

Output:

14

Callers now pass tuple([1, 2, 3]) or a literal tuple instead of a list, and caching works because tuples are hashable (as long as their contents are too).

Mistake 2: Forgetting @wraps inside a decorator

A decorator written without @wraps silently replaces the decorated function's identity:

def logged_bad(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@logged_bad
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

print(add.__name__)

This prints wrapper instead of add, which breaks stack traces, documentation generators, and any code that inspects __name__ or __doc__. The fix, shown earlier in Example 4, is to add @wraps(func) to the inner wrapper definition — a one-line change that preserves the original function's identity through the decoration.

Mistake 3: Using lru_cache on instance methods without care

Decorating a bound method with @lru_cache caches results keyed partly on self. Because the cache holds a reference to self for as long as the cache entry lives, this can keep instances alive longer than expected in long-running programs, and different instances with equal-looking state will still get separate cache entries because they are different objects. Prefer functools.cached_property for per-instance caching of a computed attribute, or cache a module-level helper function that takes only hashable, non-self data.

Best Practices

  • Use @cache (Python 3.9+) instead of @lru_cache(maxsize=None) when you want unbounded memoization — it is shorter and equally clear.
  • Set a sensible maxsize on lru_cache for long-running services so memory does not grow without bound; call .cache_info() during development to confirm the cache is actually paying off.
  • Always add @wraps(func) when writing a decorator, so __name__, __doc__, and stack traces stay accurate.
  • Only apply lru_cache to pure functions — ones whose return value depends only on their arguments and that have no side effects — otherwise you risk returning stale or incorrect cached results.
  • Prefer functools.partial over writing a tiny wrapper def just to pre-fill arguments; it is more explicit about intent and slightly cheaper.
  • Use functools.cached_property for expensive, rarely-changing instance attributes instead of hand-rolling a self._cache dictionary.
  • Reach for functools.singledispatch instead of long isinstance chains when a function needs different behavior per input type.
  • Remember that reduce is often less readable than a plain for loop or a built-in like sum()/max(); use it when the operation is genuinely a fold and no built-in already expresses it.

Practice Exercises

  1. Write a function factorial(n) decorated with @lru_cache. Call it for several values of n in a loop and print factorial.cache_info() afterward to see how many calls were cache hits.
  2. Use functools.reduce to find the maximum value in a list of dictionaries by a specific key (for example, the dictionary with the highest "score"), without using the built-in max() function.
  3. Write a decorator named retry that re-calls a wrapped function up to 3 times if it raises a ValueError, using @wraps to preserve the wrapped function's metadata. Test it on a function that fails the first two times it is called and succeeds the third time.

Summary

  • functools.reduce folds an iterable down to a single cumulative value using a two-argument function.
  • functools.lru_cache / functools.cache memoize function results keyed on their (hashable) arguments, turning exponential recursive algorithms into linear ones.
  • functools.partial creates a new callable with some arguments pre-bound, without writing a wrapper function by hand.
  • functools.wraps preserves a decorated function's __name__, __doc__, and other metadata so decorators do not break introspection.
  • functools.singledispatch, functools.total_ordering, and functools.cached_property solve type-dispatch, comparison-operator, and per-instance-caching boilerplate respectively.
  • Arguments to lru_cache-decorated functions must be hashable; caching methods keeps self alive and should be done deliberately.
  • Only cache pure functions — caching functions with side effects or non-deterministic output produces incorrect results.