functools & Functional Programming Tools
15 questions foundWhat is the functools module used for in Python?
Beginner functools provides higher-order functions that act on or return other functions, such as tools for caching (lru_cache, cache), partial application (partial), reducing sequences (reduce), and customizing comparisons (cmp_to_key, total_ordering). It centralizes common functional-programming utilities so you don't reimplement them.
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
return n if n < 2 else fib(n-1) + fib(n-2)
print(fib(30)) # cached, fast
Real-world example A pricing service caches expensive tax-rate lookups per region using lru_cache to avoid repeated database hits.
Common follow-ups: What's the difference between functools.cache and lru_cache?;When would you avoid caching a function?
Decorators;Closures
How does functools.partial work and when is it useful?
Intermediate partial creates a new callable with some positional or keyword arguments pre-filled, returning a function with a smaller remaining signature. It's useful for adapting a general function to a specific callback interface, like event handlers or map() calls, without writing a wrapper function or lambda.
from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
print(square(5)) # 25
Real-world example A GUI framework binds partial(handle_click, button_id=3) as a callback so the click handler already knows which button fired.
Common follow-ups: How is partial different from a lambda?;Can you partial a method?
Decorators;Functions & Scope
What does functools.reduce do, and why is it often discouraged in favor of loops?
Intermediate reduce(function, iterable, initializer) cumulatively applies a binary function to items of an iterable, reducing it to a single value. It's discouraged for complex logic because the accumulation is implicit and harder to read than an explicit loop or a built-in like sum() or math.prod(), which are clearer for common cases.
from functools import reduce
product = reduce(lambda acc, x: acc * x, [1,2,3,4], 1)
print(product) # 24
Real-world example A financial report combines a list of transaction deltas into a running total balance using reduce, though sum() would be clearer for simple addition.
Common follow-ups: When is reduce clearer than a for-loop?;What's the role of the initializer argument?
Comprehensions & Generators;Iterators & the Iterator Protocol
How does @functools.wraps preserve metadata when writing decorators?
Intermediate When a decorator replaces a function with a wrapper, the wrapper loses the original's __name__, __doc__, and other metadata. @functools.wraps(func) applied to the wrapper copies that metadata over, so introspection tools, help(), and debuggers still show the original function's identity.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
return func(*args, **kwargs)
return wrapper
@log_calls
def greet():
'''Say hello.'''
pass
print(greet.__name__, greet.__doc__)
Real-world example A web framework's logging decorator uses wraps so Flask's URL routing still sees the correct endpoint function name instead of 'wrapper'.
Common follow-ups: What breaks if you omit functools.wraps?;What is functools.WRAPPER_ASSIGNMENTS?
Decorators;Magic Methods & Operator Overloading
How does functools.singledispatch enable function overloading based on argument type?
Advanced singledispatch turns a function into a generic function that dispatches to type-specific implementations registered with @func.register(Type). The base function acts as the default implementation, and Python picks the most specific matching registered implementation based on the first argument's runtime type.
from functools import singledispatch
@singledispatch
def describe(obj):
return f'object: {obj}'
@describe.register
def _(obj: int):
return f'integer: {obj}'
@describe.register
def _(obj: list):
return f'list of {len(obj)} items'
print(describe(5), describe([1,2,3]))
Real-world example A serialization library uses singledispatch to convert different Python types (int, list, datetime) to JSON-compatible representations without a long if/elif chain.
Common follow-ups: How does singledispatchmethod differ for class methods?;How is dispatch resolved for subclasses?
Multiple Inheritance & MRO;Decorators
What is functools.cached_property and how does it differ from @property combined with manual caching?
Advanced cached_property computes a value once on first access and stores it in the instance's __dict__ under the same attribute name, replacing itself so subsequent lookups skip the descriptor entirely and hit the cached value directly -- faster than a property that manually checks a cache flag each call. It requires the instance to support attribute assignment (no __slots__ conflict).
from functools import cached_property
class Report:
def __init__(self, data):
self.data = data
@cached_property
def summary(self):
print('Computing...')
return sum(self.data)
r = Report([1,2,3])
print(r.summary) # Computing... 6
print(r.summary) # 6 (no recompute)
Real-world example A data model class uses cached_property to lazily compute an expensive aggregate statistic only when first requested by a report view.
Common follow-ups: Why doesn't cached_property work with __slots__ by default?;How do you invalidate a cached_property value?
Descriptors & Properties;Memory Management & Garbage Collection
How does functools.total_ordering reduce boilerplate for comparison methods?
Intermediate total_ordering takes a class that defines __eq__ and one of __lt__, __le__, __gt__, or __ge__, then fills in the remaining comparison methods automatically. This avoids manually writing all six rich comparison methods for classes that need full ordering support.
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, num):
self.num = num
def __eq__(self, other):
return self.num == other.num
def __lt__(self, other):
return self.num < other.num
print(Version(1) <= Version(2)) # True, auto-derived
Real-world example A package manager's Version class uses total_ordering to support sorting release versions without writing __le__, __gt__, and __ge__ by hand.
Common follow-ups: Why is total_ordering slower than writing all methods manually?;What happens if __eq__ is missing?
Magic Methods & Operator Overloading;Data Types & Structures
What is the difference between functools.cache and functools.lru_cache?
Beginner functools.cache (added in Python 3.9) is a simpler, unbounded version of lru_cache -- equivalent to lru_cache(maxsize=None). Use cache when you want unlimited caching with simpler syntax, and lru_cache when you need to bound memory usage with maxsize or need typed caching.
from functools import cache
@cache
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
print(factorial(10))
Real-world example A configuration loader caches parsed config file contents with @cache since the file rarely changes during a program's run.
Common follow-ups: When would unbounded caching cause memory problems?;How do you clear a cached function's cache?
functools & Functional Programming Tools;Memory Management & Garbage Collection
How do you clear or inspect the cache of an lru_cache-decorated function?
Advanced Every lru_cache-wrapped function gets cache_info() (returns hits, misses, maxsize, currsize) and cache_clear() (empties the cache) methods attached. This is useful for debugging cache effectiveness or resetting state between test runs.
from functools import lru_cache
@lru_cache(maxsize=128)
def square(x):
return x * x
square(4); square(4); square(5)
print(square.cache_info()) # CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
square.cache_clear()
Real-world example A test suite calls cache_clear() in a pytest fixture teardown to ensure cached results from one test don't leak into another.
Common follow-ups: Is lru_cache thread-safe?;Can you cache methods with lru_cache safely?
Debugging & Profiling;functools & Functional Programming Tools
Why is caution needed when applying @lru_cache directly to instance methods?
Advanced lru_cache stores a reference to every argument, including self, as part of the cache key. Applied to an instance method, this keeps instances alive as long as the cache holds entries, potentially causing memory leaks, and cache entries are shared across all instances by argument value rather than being per-instance.
from functools import lru_cache
class Service:
@lru_cache(maxsize=None)
def compute(self, x): # caches (self, x) pairs -- keeps self alive
return x * 2
Real-world example A long-running server process notices memory growth traced to a cached instance method holding references to short-lived request objects via self.
Common follow-ups: How can you cache per-instance without leaking with lru_cache?;What's an alternative using cached_property?
Memory Management & Garbage Collection;Descriptors & Properties