Lesson 32 · Intermediate · 19 min

Python functools

Reuse function behavior with caching, partial application, and reduction.

functoolslru_cachepartialreduce

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.
Your turn

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.

In-browser PythonRuns locally · no account
First run downloads Pyodide from a CDN. Use Run code after editing.

Practice this lesson

12 questions with immediate explanations.

See all 12 answersStart practice
← collectionsenums →