Skip to content

functools Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on functools — lru_cache and cache, partial, wraps, reduce, cached_property, and singledispatch.

Read the in-depth guidePython functools Explained — lru_cache, partial, reduce, wraps, and cached_property(opens in new tab)
15 of 15

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel