The price lookup runs thousands of times a day and never changes. Caching it is one line. When does a cache quietly become a bug?
functools collects tools that operate on functions. lru_cache remembers results, partial fixes some arguments in advance, and reduce folds a sequence into one value. They are small additions that can remove repetitive wrapper code. You will also inspect cache behavior, bind keyword arguments, and dispatch on type.
Cache repeated calls
lru_cache stores results by arguments. The second call with the same arguments can return the stored value without running the body again. Use maxsize=None when the cache should grow without a fixed limit.
from functools import lru_cache
@lru_cache(maxsize=None)
def square(n):
print("calculating", n)
return n * n
print(square(3))
print(square(3))Calculate once, reuse twice
Fix arguments with partial
partial creates a new callable with some arguments already filled in. The remaining arguments are supplied later. This is useful for adapting a general function to a specific use, such as a square function built from a power function.
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
print(square(5))A specialized function
Fold with reduce
reduce applies a two-argument function cumulatively from left to right. The optional initial value seeds the accumulator. Use it when the operation is naturally a fold; a clear loop is often better when the logic is complex.
from functools import reduce
print(reduce(lambda total, n: total + n, [1, 2, 3], 0))Many values, one result
Cache only pure, hashable calls
lru_cache uses the arguments as a key, so every argument must be hashable. It also assumes the function is pure: the same input should produce the same output. cache_info() shows hits and misses; cache_clear() is useful in tests when state changes.
from functools import lru_cache
calls = []
@lru_cache(maxsize=None)
def delivery_fee(city, weight):
calls.append(city)
return 4 if weight <= 2 else 7
print(delivery_fee("Taipei", 1.5))
print(delivery_fee("Taipei", 1.5))
print(delivery_fee("Tainan", 1.5))
print(len(calls), delivery_fee.cache_info().hits)A hit avoids the function body
Bind a keyword with partial
partial fills in positional or keyword arguments and returns a new callable. This is handy for callbacks that always need the same prefix or formatter. The original function is untouched, and partial keeps references to the bound arguments for inspection.
from functools import partial
def render(template, name, *, prefix=""):
return f"{prefix}{template.format(name=name)}"
greet = partial(render, "Hello, {name}!", prefix="> ")
print(greet("Maya"))
print(greet.func.__name__, greet.args, greet.keywords)A reusable callable with fixed context
Dispatch on type with singledispatch
singledispatch chooses an implementation from the first argument's type. The generic function is the fallback; register() adds more specific handlers. It keeps type branching out of a long if/elif chain, but it is not a replacement for validating untrusted input.
from functools import singledispatch
@singledispatch
def label(value):
return "other"
@label.register
def _(value: int):
return f"int:{value}"
@label.register
def _(value: str):
return f"str:{value}"
print(label(3))
print(label("tea"))
print(label(3.5))One name, several typed handlers
Key points
- lru_cache caches results by the function arguments.
- The cache must be cleared when results depend on changing external state.
- partial fixes arguments and returns a new callable.
- reduce folds an iterable into one value.
- A short loop can be clearer than reduce for complex logic.
- Cache pure hashable calls; partial fixes context; singledispatch replaces type branches.
Common mistakes
- Caching a function whose result depends on mutable global state.
- Expecting partial to call the function immediately.
- Forgetting the initial value in reduce when the accumulator type matters.
- Caching a function that reads mutable global state and expecting automatic invalidation.
Cache a square function with lru_cache, then build a square function with partial(power, exponent=2).
Try it yourself
Change the values, predict the output, then run the code. The Python runtime is downloaded the first time you press Run, so the first run can take a few seconds.
Practice this lesson
12 questions with immediate explanations.