functools

The functools module is the standard library's toolbox for higher-order functions — partial application, memoized caching, generic dispatch by type, and utilities for writing well-behaved decorators.

functools collects tools for working with functions and callables as first-class objects: pre-filling some of a function's arguments, caching expensive results, dispatching a function's behavior based on argument type, computing a property only once, and preserving a wrapped function's identity through a decorator. These are among the most commonly used functional-programming utilities in everyday Python code.

functools.partial — Pre-Filling Arguments

partial(func, *args, **kwargs) returns a new callable with some arguments already bound, so calling the result only requires the remaining ones. This is useful for specializing a general function into a more specific one without writing a wrapper def.

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))  # 25.0
print(cube(2))    # 8.0

functools.reduce — Folding a Sequence to One Value

reduce(function, iterable, initializer=...) repeatedly applies a two-argument function, carrying an accumulator forward, until a single result remains. (See the dedicated map, filter, reduce topic for a full treatment; here it's covered as one of functools's core tools.)

from functools import reduce

numbers = [1, 2, 3, 4]
total = reduce(lambda acc, n: acc + n, numbers, 0)
print(total)  # 10

functools.lru_cache — Memoization with Eviction

@lru_cache(maxsize=...) memoizes a function's return values keyed by its arguments, so repeated calls with the same arguments skip recomputation and return the cached result instantly. "LRU" (least recently used) means once the cache reaches maxsize, the least-recently-used entry is evicted to make room for a new one.

Arguments to an lru_cache-wrapped function must be hashable, since they're used as dict-like cache keys — calling it with an unhashable argument like a list raises TypeError.

from functools import lru_cache

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

print(fibonacci(30))          # fast — cached subcalls avoid exponential blowup
print(fibonacci.cache_info())  # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
fibonacci.cache_clear()        # manually clear the cache if needed

functools.cache — Unbounded Memoization

@cache (added in Python 3.9) is a simpler, unbounded version of lru_cache — equivalent to lru_cache(maxsize=None), meaning nothing is ever evicted. Use it when you want to cache every distinct call forever (bounded only by memory) rather than worry about an eviction policy.

from functools import cache

@cache
def factorial(n: int) -> int:
    return 1 if n <= 1 else n * factorial(n - 1)

print(factorial(10))  # 3628800 — every distinct n's result is cached permanently

functools.cached_property — Computed Once Per Instance

@cached_property (3.8+) turns a method into a property that is computed once per instance, on first access, and then stored as a regular instance attribute — subsequent accesses skip recomputation entirely. Unlike @property, the computation does not re-run on every access.

cached_property requires the instance to support attribute assignment (i.e. no __slots__ blocking it, unless __dict__ is explicitly kept), since the cached value is stored directly in the instance's __dict__ after first access.

from functools import cached_property

class DataSet:
    def __init__(self, values: list[float]) -> None:
        self.values = values

    @cached_property
    def average(self) -> float:
        print("computing average...")
        return sum(self.values) / len(self.values)

ds = DataSet([1.0, 2.0, 3.0, 4.0])
print(ds.average)  # prints "computing average..." then 2.5
print(ds.average)  # 2.5 — no recomputation, no printed message this time

functools.wraps — Preserving Function Identity in Decorators

When writing a decorator, the returned wrapper function normally shadows the original function's __name__, __doc__, and other metadata. @wraps(func) (applied to the inner wrapper) copies that metadata over, so tools like help(), debuggers, and introspection see the original function's identity rather than a generic "wrapper".

from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

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

print(greet.__name__)  # greet — not "wrapper", thanks to @wraps
print(greet.__doc__)   # Return a friendly greeting.

functools.singledispatch — Generic Functions by Type

@singledispatch turns a function into a generic function whose implementation is chosen based on the TYPE of its first argument. Register additional type-specific implementations with @name.register. This gives Python a lightweight form of function overloading based on runtime type.

from functools import singledispatch

@singledispatch
def describe(value) -> str:
    return f"a value: {value!r}"

@describe.register
def _(value: int) -> str:
    return f"an integer: {value}"

@describe.register
def _(value: list) -> str:
    return f"a list with {len(value)} items"

print(describe(42))          # an integer: 42
print(describe([1, 2, 3]))   # a list with 3 items
print(describe(3.14))        # a value: 3.14 — falls back to the default implementation

functools.total_ordering — Filling In Comparison Methods

@total_ordering fills in the remaining rich-comparison methods (__le__, __gt__, __ge__) from just __eq__ plus one of them (e.g. __lt__), saving you from writing all four by hand.

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major: int, minor: int) -> None:
        self.major, self.minor = major, minor

    def __eq__(self, other: object) -> bool:
        return (self.major, self.minor) == (other.major, other.minor)

    def __lt__(self, other: "Version") -> bool:
        return (self.major, self.minor) < (other.major, other.minor)

v1, v2 = Version(1, 2), Version(1, 5)
print(v1 < v2, v1 <= v2, v1 > v2, v1 >= v2)  # True True False False

Practical Usage

functools tools show up constantly in real code: lru_cache/cache for speeding up pure, repeatable computations (parsing, expensive lookups); partial for adapting a general callback into a specific one (e.g. for GUI event handlers or map()); wraps in essentially every hand-written decorator; cached_property for expensive derived attributes computed once; and singledispatch when a function's logic genuinely branches by argument type.

Edge Cases and Rules

lru_cache/cache require all arguments to be hashable — unhashable arguments (like a list or dict) raise TypeError. Cached functions should be effectively pure (same input -> same output, no meaningful side effects), since a cache hit skips re-running the function body entirely, including any side effects it would otherwise repeat. cached_property caches per INSTANCE, not globally — different instances get independently computed and cached values.

Key Takeaways

partial() pre-binds some arguments of a function, producing a new, more specific callable.

lru_cache/cache memoize function results by argument; arguments must be hashable, and cached functions should be effectively pure.

cached_property computes a derived attribute once per instance on first access, then reuses the stored value.

wraps(func) inside a decorator preserves the wrapped function's __name__/__doc__ for introspection — use it in essentially every hand-written decorator.

singledispatch provides type-based generic function dispatch, and total_ordering fills in missing comparison methods from a minimal set you define.

def log_calls(func): def wrapper(*args, **kwargs): # missing @wraps return func(*args, **kwargs) return wrapper @log_calls def greet(name):... print(greet.__name__) # "wrapper" — original identity lost from functools import wraps def log_calls(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @log_calls def greet(name):... print(greet.__name__) # "greet" — identity preserved

Omitting @wraps in a decorator's inner function silently replaces the original function's __name__/__doc__/metadata with the generic wrapper's, breaking introspection, help(), and some frameworks that rely on function identity.

from functools import lru_cache @lru_cache def process(items: list) -> int: return sum(items) process([1, 2, 3]) # TypeError: unhashable type: 'list' from functools import lru_cache @lru_cache def process(items: tuple) -> int: return sum(items) process((1, 2, 3)) # OK — tuples are hashable lru_cache uses its arguments as cache keys, which requires them to be hashable — pass an immutable, hashable type like a tuple instead of a list.

from functools import lru_cache import random @lru_cache def roll_die(): return random.randint(1, 6) # impure! caching hides the randomness print(roll_die(), roll_die(), roll_die()) # same value three times — surprising import random def roll_die(): return random.randint(1, 6) # no caching — genuinely random each call print(roll_die(), roll_die(), roll_die()) # independently random

Caching an impure function (one with side effects or non-deterministic output) freezes its first result for all future calls with the same arguments, which is almost never the intended behavior.

What does functools.partial do, and give a scenario where it's useful?

It returns a new callable with some of a function's arguments pre-bound, useful for specializing a general function (e.g. power(base, exponent)) into a more specific one (e.g. square = partial(power, exponent=2)) without writing a new def.

What is the difference between functools.cache and functools.lru_cache?

functools.cache is an unbounded cache (equivalent to lru_cache(maxsize=None)) that never evicts entries; lru_cache lets you set a maxsize and evicts the least-recently-used entry once that limit is reached.

Why must arguments to an lru_cache-wrapped function be hashable?

Because lru_cache uses the arguments as dict-like cache keys internally; unhashable types like list or dict cannot serve as keys, so calling the function with one raises TypeError.

What does @wraps(func) do inside a decorator, and what breaks if you omit it?

It copies the original function's __name__, __doc__, and other metadata onto the wrapper function. Without it, introspection tools (help(), debuggers, some frameworks) see the generic wrapper's identity instead of the original function's.

How does cached_property differ from an ordinary @property?

@property recomputes its value on every access; @cached_property computes the value once on first access per instance, stores it directly in the instance's __dict__, and reuses that stored value on subsequent accesses without recomputing.

What does functools.singledispatch let you do that a normal function cannot?

It lets you register multiple type-specific implementations of a function under one name, dispatching to the correct implementation based on the runtime type of the first argument — a lightweight form of function overloading by type.

Why is it risky to cache (with lru_cache/cache) a function that is not pure — e.g. one that reads a clock or a random number generator?

Caching freezes the first result for a given set of arguments and returns that stale result on every subsequent call with the same arguments, silently hiding the function's real (time-varying or random) behavior — which is almost never what's intended for an impure function.

Write a function power(base, exponent), then use functools.partial to create cube(base) that always uses exponent=3.

from functools import partial def power(base: float, exponent: float) -> float: return base ** exponent cube = partial(power, exponent=3) print(cube(4)) # 64.0

64.0 — partial binds exponent=3 ahead of time, so cube(4) calls power(4, exponent=3).

What does this print, and why?

3 — square(2) and square(3) are cache misses (calls becomes 2), square(2) again is a cache hit (no increment), and square(4) is a new argument so it's a miss (calls becomes 3); with maxsize=2, adding square(4) evicts the least-recently-used cached entry to make room.

Write a decorator announce that prints 'starting <name>' before calling the wrapped function, using @wraps to preserve the original function's __name__. Apply it to a function add(a, b) and verify add.__name__ still reads 'add'.

from functools import wraps def announce(func): @wraps(func) def wrapper(*args, **kwargs): print(f"starting {func.__name__}") return func(*args, **kwargs) return wrapper @announce def add(a: int, b: int) -> int: return a + b print(add(2, 3)) # prints "starting add" then 5 print(add.__name__) # "add" — preserved thanks to @wraps add(2, 3) prints 'starting add' then returns 5; add.__name__ is 'add' rather than 'wrapper' because @wraps(func) copied the original function's metadata onto the wrapper.

from functools import lru_cache

calls = 0

@lru_cache(maxsize=2)
def square(n):
    global calls
    calls += 1
    return n * n

square(2)
square(3)
square(2)   # cache hit
square(4)   # evicts least-recently-used entry
print(calls)