functools.lru_cache is a decorator that memoizes a function — it stores
results keyed by the arguments and returns the cached value on repeat calls,
avoiding recomputation. maxsize caps how many results are kept, evicting the
least-recently-used entries; lru_cache(maxsize=None) (or functools.cache
in 3.9+) caches without limit.
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(50) # fast — each n computed once
fib.cache_info() # hits, misses, maxsize, currsize
fib.cache_clear() # reset the cache
Arguments must be hashable (they're used as dict keys), and the function should be pure — caching an impure function returns stale results. Rule of thumb: use it for expensive, deterministic calls with repeated inputs.
functools.partial creates a new callable with some arguments of an existing
function pre-filled. It's a clean way to specialize a general function without
writing a wrapper or a lambda.
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2) # exp fixed to 2
cube = partial(power, exp=3)
square(5) # 25
cube(2) # 8
Partials are handy for callbacks, event handlers, and configuring functions passed
to map/sorted/GUI bindings. Rule of thumb: reach for partial when you keep
calling the same function with one or two fixed arguments.
A decorator replaces the original function with a wrapper, which loses the
original's metadata — its __name__, __doc__, and signature now point at the
wrapper. functools.wraps copies that metadata from the wrapped function onto
the wrapper, so introspection, debugging, and documentation still work.
from functools import wraps
def log(fn):
@wraps(fn) # copy fn's metadata to wrapper
def wrapper(*args, **kwargs):
print("calling", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@log
def greet(): "say hi"
greet.__name__ # 'greet' (without @wraps it'd be 'wrapper')
Without @wraps, tools like help(), tracebacks, and doc generators show the
wrapper instead of the real function. Rule of thumb: always add @wraps(fn) to the
inner function of any decorator.
functools.reduce repeatedly applies a two-argument function across an iterable,
folding it down to a single accumulated value. It carries a running result,
combining it with each element in turn; an optional initializer seeds the
accumulator (and makes it safe on empty iterables).
from functools import reduce
reduce(lambda acc, x: acc + x, [1, 2, 3, 4]) # 10
reduce(lambda acc, x: acc * x, [1, 2, 3, 4], 1) # 24, seeded with 1
For common folds Python already has built-ins (sum, max, min, any, all)
that are clearer and faster — reduce shines for custom accumulation logic. Rule
of thumb: prefer a built-in or an explicit loop unless the fold is genuinely
bespoke, since reduce can hurt readability.
functools.cached_property turns a method into a property whose result is
computed once and stored on the instance, so later accesses are cheap. The
cached value lives in the instance __dict__ and is recomputed only if you delete
it. functools.singledispatch creates a generic function that dispatches to
different implementations based on the type of the first argument — function
overloading by type.
from functools import cached_property, singledispatch
class Dataset:
@cached_property
def stats(self): # expensive; runs once per instance
return expensive_scan(self.data)
@singledispatch
def describe(x): return f"value: {x}"
@describe.register
def _(x: list): return f"list of {len(x)}"
@describe.register
def _(x: int): return f"int {x}"
describe([1, 2]) # 'list of 2'
describe(7) # 'int 7'
cached_property trades memory for speed on costly, stable computations;
singledispatch keeps type-specific behaviour in separate, registerable functions
instead of a big if/isinstance chain. Rule of thumb: cache derived values that
don't change, and dispatch when behaviour varies cleanly by argument type.
@cache (3.9+) is an unbounded memoizer — shorthand for
lru_cache(maxsize=None). @lru_cache(maxsize=N) keeps only the N most
recent results, evicting the least-recently-used. Unbounded is simpler and
slightly faster but can grow memory without limit.
from functools import cache, lru_cache
@cache # never evicts
def fib(n): ...
@lru_cache(maxsize=128) # bounded
def fetch(url): ...
Rule of thumb: use cache for small/finite key spaces; lru_cache(maxsize)
when the key space is large and you must cap memory.
All arguments must be hashable, because the cache keys on them — so you
can't memoize a function taking a list or dict. The cached function also
exposes .cache_info() (hits/misses) and .cache_clear(). Mutable
default results are shared, so don't mutate returned objects.
from functools import lru_cache
@lru_cache
def f(x): ...
f([1, 2]) # TypeError: unhashable type: 'list'
f.cache_info() # CacheInfo(hits=.., misses=.., maxsize=.., currsize=..)
f.cache_clear()
Rule of thumb: only memoize pure functions with hashable args; convert lists to tuples before calling.
It fills in the missing rich-comparison methods from the ones you define.
Provide __eq__ plus one of __lt__/__le__/__gt__/__ge__, and the
decorator generates the rest. Saves boilerplate, at a small performance cost
versus writing them all by hand.
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, n): self.n = n
def __eq__(self, o): return self.n == o.n
def __lt__(self, o): return self.n < o.n
# __le__, __gt__, __ge__ generated automatically
Rule of thumb: use total_ordering to make a class fully orderable from just
__eq__ and one ordering method.
Like partial, but for methods in a class body — it pre-binds arguments
while still receiving self correctly. Handy for generating related methods
(e.g. setters with a fixed state) without repetitive wrappers.
from functools import partialmethod
class Cell:
def set_state(self, state): self.state = state
activate = partialmethod(set_state, True)
deactivate = partialmethod(set_state, False)
c = Cell(); c.activate() # self.state = True
Rule of thumb: use partialmethod to derive specialized methods from a
general one inside a class.
The third argument seeds the accumulator. Without it, reduce raises
TypeError on an empty iterable and uses the first element as the seed.
An initializer gives a safe default for empty input and sets the result type.
from functools import reduce
reduce(lambda a, b: a + b, [], 0) # 0, not an error
reduce(lambda a, b: a + b, []) # TypeError: empty iterable
reduce(lambda acc, x: acc | {x}, items, set()) # build a set
Rule of thumb: always pass an initializer to reduce when the iterable might
be empty or you want a specific starting type.
Decorate the generic function with @singledispatch, then add type-specific
versions with @func.register (annotate the first parameter's type or pass
it explicitly). Dispatch is on the first argument's type. There's also
singledispatchmethod for methods.
from functools import singledispatch
@singledispatch
def show(x): return str(x)
@show.register
def _(x: list): return ", ".join(map(str, x))
@show.register(int)
def _(x): return f"int:{x}"
Rule of thumb: use singledispatch to add type-based behavior to a function
without a chain of isinstance checks.
@property recomputes on every access; @cached_property computes
once, then stores the result in the instance __dict__, returning it
directly thereafter. It needs a writable __dict__ (so it doesn't work with
__slots__) and isn't recomputed if dependencies change.
from functools import cached_property
class Dataset:
@cached_property
def stats(self):
return expensive_scan(self.data) # computed on first access only
Rule of thumb: use cached_property for expensive, stable derived values;
use property when the value can change between accesses.
It copies the wrapped function's __name__, __doc__, __module__,
__qualname__, __dict__, and __annotations__ onto the wrapper, and sets
__wrapped__ to point at the original. Without it, decorated functions
report the wrapper's name and lose their docstring, breaking introspection and
help().
from functools import wraps
def log(fn):
@wraps(fn)
def inner(*a, **k):
return fn(*a, **k)
return inner
@log
def greet(): "say hi"
greet.__name__ # 'greet', not 'inner'
greet.__wrapped__ # the original function
Rule of thumb: always apply @wraps(fn) to the inner function in a decorator
to preserve metadata.
partial binds arguments at definition time and is picklable,
introspectable (.func, .args, .keywords), and avoids the late-binding
closure trap of lambdas in loops. A lambda re-evaluates free variables when
called, which can surprise you.
from functools import partial
# lambda late-binding bug:
fns = [lambda: i for i in range(3)] # all return 2
# partial captures now:
fns = [partial(lambda x: x, i) for i in range(3)] # 0, 1, 2
Rule of thumb: prefer partial when you need to pre-bind args reliably
(loops, callbacks, pickling); a lambda is fine for trivial inline logic.
When a built-in or comprehension is clearer: use sum, min, max,
math.prod, "".join, all/any instead of reduce. reduce is justified
only for genuinely custom accumulations with no built-in equivalent — and even
then a plain loop is often more readable.
from functools import reduce
reduce(lambda a, b: a + b, nums) # just use sum(nums)
reduce(lambda a, b: a * b, nums) # use math.prod(nums)
Rule of thumb: reach for a built-in first; use reduce only for non-standard
folds, and prefer a loop if it reads more clearly.
More Functional Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.