functools is a standard library module for higher-order functions — functions that act on or return other functions. Its most-used members are lru_cache/cache (memoisation decorators), partial (partial application), reduce (fold a sequence to a single value), and wraps (preserve metadata when writing decorators).
Tools for working with functions
What you'll learn: The functools toolbox, and especially @lru_cache/@cache — decorators that make slow functions fast by remembering their results (memoisation).
How to read this tab: Write the naive recursive fibonacci, time fib(35), then add @cache and time it again. The speedup — from seconds to instant — is the most memorable way to understand memoisation.
functools is a toolbox for working with functions themselves. The two tools you'll reach for most: @lru_cache to make slow functions fast by remembering results, and partial to create a new function with some arguments pre-filled.
functools is the standard library's function toolbox. The high-value tools: @lru_cache/@cache (memoisation — the biggest performance win for the least effort), partial (preset arguments), @wraps (covered on the Decorators page — mandatory when writing decorators). Learn lru_cache first; it solves real performance problems with one line.
What functools provides
functools is part of the standard library — no installation needed. It contains higher-order functions: tools that take functions as arguments or return new functions.
| Tool | What it does |
|---|---|
@lru_cache | Cache a function's results (memoisation) |
@cache | Simpler unbounded cache (Python 3.9+) |
partial() | Create a new function with some arguments fixed |
reduce() | Combine a sequence into a single value |
@wraps | Preserve metadata when writing decorators |
@singledispatch | Function overloading based on argument type |
cmp_to_key() | Convert old-style comparison functions to keys |
@lru_cache is the single highest-impact tool in functools. Adding one decorator line can turn an exponential-time recursive function into a linear-time one. The rule: apply it to pure functions (same input always gives same output) that are called repeatedly with the same arguments. Never apply it to functions with side effects or those whose results change over time.
Caching with lru_cache and cache
The single most impactful functools tool: @lru_cache remembers the results of function calls. When the function is called again with the same arguments, the cached result is returned instantly instead of recomputing.
from functools import lru_cache, cache
# Without caching — fibonacci is exponentially slow
def fib_slow(n):
if n < 2: return n
return fib_slow(n-1) + fib_slow(n-2)
# fib_slow(35) makes ~30 million calls — takes seconds
# With caching — each value computed once
@lru_cache(maxsize=None) # None = unbounded cache
def fib_fast(n):
if n < 2: return n
return fib_fast(n-1) + fib_fast(n-2)
# fib_fast(35) makes 36 calls — instant
print(fib_fast(100)) # 354224848179261915075 — instant
# @cache (Python 3.9+) is shorthand for @lru_cache(maxsize=None)
@cache
def expensive_lookup(key):
print(f"Computing for {key}...")
return key ** 2
print(expensive_lookup(5)) # Computing for 5... → 25
print(expensive_lookup(5)) # 25 (no "Computing" — cached)
# Inspect cache performance
print(fib_fast.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)
# Clear the cache if needed
fib_fast.cache_clear()@lru_cache stores results in a dict keyed by the arguments — so all arguments must be hashable. You can cache f(1, "x") but not f([1,2,3]) because lists aren't hashable. Pass tuples instead of lists for cacheable functions.
✅ Beginner tab complete
- I can apply @lru_cache or @cache to memoise an expensive function
- I know cached functions need hashable arguments (tuples, not lists)
- I can inspect cache performance with func.cache_info()
- I know @cache (3.9+) is shorthand for @lru_cache(maxsize=None)
partial, reduce, and singledispatch
What you'll learn: partial for creating specialised functions with preset arguments. reduce for folding sequences. singledispatch for type-based function overloading.
How to read this tab: Write a log function, then create info = partial(log, "INFO") and error = partial(log, "ERROR"). This pattern — preset configuration via partial — appears throughout real codebases.
partial creates a new function with some arguments locked in. It's cleaner than a lambda for this purpose: partial(log, "INFO") is clearer than lambda msg: log("INFO", msg). partial also plays well with introspection tools, whereas lambdas are anonymous. Use partial when you need to preset arguments; use lambda for arbitrary short inline functions.
partial application and reduce
partial creates a new function with some arguments already filled in. reduce combines all elements of a sequence into a single value by repeatedly applying a function.
from functools import partial, reduce
# partial — fix some arguments to create a specialised function
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2) # exponent is now fixed at 2
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(3)) # 27
# Real-world: partial for callbacks with preset config
def log(level, message):
print(f"[{level}] {message}")
info = partial(log, "INFO")
error = partial(log, "ERROR")
info("Server started") # [INFO] Server started
error("Connection lost") # [ERROR] Connection lost
# reduce — fold a sequence into one value
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers) # ((((1+2)+3)+4)+5)
print(total) # 15
product = reduce(lambda acc, x: acc * x, numbers) # 5! = 120
print(product) # 120
# reduce with an initial value
total = reduce(lambda acc, x: acc + x, numbers, 100) # starts at 100
print(total) # 115
# Note: for sum/product, prefer the built-ins — they're clearer
print(sum(numbers)) # 15 — clearer than reduce
import math
print(math.prod(numbers)) # 120 — clearer than reduce@singledispatch brings type-based overloading to Python. Other languages let you define multiple functions with the same name but different parameter types. Python doesn't — but @singledispatch simulates it for the first argument's type. Register a separate implementation per type; Python picks the right one at call time based on the argument's type.
singledispatch — function overloading by type
@singledispatch lets you write a function that behaves differently based on the type of its first argument — Python's version of function overloading.
from functools import singledispatch
@singledispatch
def describe(value):
"""Default implementation."""
return f"Unknown type: {value}"
@describe.register
def _(value: int):
return f"Integer: {value} (binary: {value:b})"
@describe.register
def _(value: str):
return f"String of length {len(value)}: {value!r}"
@describe.register
def _(value: list):
return f"List with {len(value)} items"
print(describe(42)) # Integer: 42 (binary: 101010)
print(describe("hello")) # String of length 5: 'hello'
print(describe([1, 2, 3])) # List with 3 items
print(describe(3.14)) # Unknown type: 3.14 (default)✅ Intermediate tab complete
- I can use partial() to fix some arguments and create a specialised function
- I can use reduce() to combine a sequence into a single value
- I know to prefer sum()/math.prod() over reduce() when they apply
- I can use @singledispatch for type-based overloading
cached_property, total_ordering, and LRU internals
What you'll learn: @cached_property for compute-once instance attributes. @total_ordering to generate all comparison methods from two. How lru_cache implements O(1) LRU eviction with a linked list and dict.
How to read this tab: Implement a class with @cached_property for an expensive computed attribute. Note that it computes once per instance, then caches.
cached_property, total_ordering, and the wraps protocol
functools includes several class-oriented tools. @cached_property computes a property once and caches it on the instance. @total_ordering generates all comparison methods from just __eq__ and one other. @wraps (covered fully on the Decorators page) copies the wrapped function's metadata.
from functools import cached_property, total_ordering
class Dataset:
def __init__(self, data):
self.data = data
@cached_property
def statistics(self):
"""Computed once, then cached on the instance."""
print("Computing statistics...")
import statistics
return {
"mean": statistics.mean(self.data),
"stdev": statistics.stdev(self.data),
}
ds = Dataset([1, 2, 3, 4, 5])
print(ds.statistics) # Computing statistics... → {...}
print(ds.statistics) # {...} — no recomputation, cached on instance
# @total_ordering — define __eq__ and one comparison, get the rest free
@total_ordering
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
# total_ordering generates __le__, __gt__, __ge__ automatically
v1 = Version(1, 2)
v2 = Version(1, 5)
print(v1 < v2) # True
print(v1 >= v2) # False — generated from __eq__ and __lt__
print(v1 <= v2) # True — generatedThe implementation of lru_cache uses a circular doubly-linked list plus a dict to implement the Least-Recently-Used eviction policy in O(1) per access. When the cache reaches maxsize, the least recently used entry is evicted. With maxsize=None (or @cache), no eviction occurs — the cache grows without bound, so use it only when the set of distinct argument combinations is finite.
✅ Expert tab complete
- I can use @cached_property for a compute-once instance attribute
- I can use @total_ordering to generate comparison methods from __eq__ and __lt__
- I know lru_cache uses a doubly-linked list + dict for O(1) LRU eviction