Pythonfunctools

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.

Note
This pairs especially well with **recursive functions**, which we covered on the recursion page earlier in this course. Naive recursion often recomputes the same sub-problem many times; caching those results turns an exponential amount of work into a linear amount.

The classic example is a naive recursive Fibonacci function, which recalculates the same values over and over as the input grows:

Python
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:

Python
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.

Warning
`lru_cache` only works on functions whose arguments are hashable (numbers, strings, tuples, etc.) — you can't cache a call made with a `list` or `dict` argument. Also be careful caching methods that depend on mutable state, since the cache has no way of knowing that state has changed.

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:

Python
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:

Python
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.

Python
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.

Python
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']
Tip
If you're writing new code, prefer a plain `key=` function when possible — it's usually simpler and faster. Reach for `cmp_to_key()` mainly when adapting existing comparison-style logic you don't want to rewrite.
`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.

Python
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

lru_cache

Memoize results for repeated calls with the same arguments

This page

reduce

Fold a sequence down to a single accumulated value

The map/filter/reduce page

partial

Pre-fill some arguments to create a specialized function

The partial-functions page

wraps

Preserve __name__/__doc__ when writing a decorator

The decorators page

cmp_to_key

Adapt an old two-argument comparator into a key= function

This page

singledispatch

Overload a function based on its first argument’s type

This page

  • lru_cache trades memory for speed — great for pure functions with a limited, repeating set of inputs, especially recursive ones.

  • reduce, partial, and wraps are everyday tools with their own dedicated pages in this course; functools bundles them alongside the more advanced tools above.

  • singledispatch only dispatches on the type of the first argument — for multiple-argument dispatch, look at the related singledispatchmethod or third-party multiple-dispatch libraries.