The functools module in the Python standard library provides tools for working with higher-order functions: functions that take other functions as arguments or return them as results. Use functools to cache expensive computations, pre-fill function arguments, compose functions, and preserve function metadata when writing decorators.
from functools import cache
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 354224848179261915075@cache automatically stores the result of every call. Without it, fibonacci(100) would take an impossibly long time because of exponential repeated work. With caching, each fibonacci(n) is computed exactly once.
Caching with cache and lru_cache
functools.cache (Python 3.9+) memoizes all calls without an upper bound. functools.lru_cache limits the cache size and evicts the least recently used entries once the cache is full.
from functools import lru_cache
call_count = 0
@lru_cache(maxsize=4)
def slow_square(n):
global call_count
call_count += 1
return n * nCall slow_square with a mix of repeated and new values, then check how many actually triggered a computation versus how many hit the cache:
for x in [1, 2, 3, 4, 1, 2, 5, 6, 1]:
slow_square(x)
print(f"Actual computations: {call_count}")
print(slow_square.cache_info())Even though slow_square was called nine times, only six of them were unique inputs, and the cache_info() line confirms exactly that split between hits and misses:
Actual computations: 6
CacheInfo(hits=3, misses=6, maxsize=4, currsize=4)Only 6 unique values triggered actual computation. The 3 repeated values hit the cache. cache_info() shows the hit/miss statistics.
Key differences:
| Feature | cache | lru_cache(maxsize=N) |
|---|---|---|
| Cache size | Unlimited | Limited to N entries |
| Eviction | None (grows forever) | Least recently used |
| Memory | Can grow large | Bounded |
| Use case | Few unique inputs | Many unique inputs |
Use cache when the number of unique inputs is small or bounded. Use lru_cache when many different inputs are possible and memory is a concern.
Partial application with partial
functools.partial fixes some arguments of a function, creating a new function that requires fewer arguments.
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125partial(power, exponent=2) creates a new function where exponent is always 2. The caller only provides the remaining argument. This is useful for adapting function signatures to match callback expectations or simplifying repeated calls with the same arguments.
import json
from functools import partial
pretty_json = partial(json.dumps, indent=2, sort_keys=True)
print(pretty_json({"c": 3, "a": 1, "b": 2}))Even though the keys were passed in c, a, b order, sort_keys=True reorders them alphabetically in the formatted output, and indent=2 adds the readable line breaks and spacing:
{
"a": 1,
"b": 2,
"c": 3
}pretty_json is a simplified version of json.dumps with preset formatting options.
reduce: combining an iterable
functools.reduce applies a function cumulatively to items of an iterable, reducing it to a single value.
from functools import reduce
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, n: acc + n, numbers)
product = reduce(lambda acc, n: acc * n, numbers)
maximum = reduce(lambda acc, n: acc if acc > n else n, numbers)
print(total, product, maximum)Each reduce call folds the list into a single number using a different combining function, producing the sum, product, and maximum in that order:
15 120 5reduce works step by step: it calls the function with the accumulated result so far and the next item. For summing and finding maximums, built-in sum() and max() are clearer. Use reduce for custom accumulation logic that has no dedicated built-in.
from functools import reduce
words = ["Hello", "functional", "Python"]
sentence = reduce(lambda acc, w: f"{acc} {w}", words)
print(sentence) # Hello functional PythonPreserving metadata with wraps
functools.wraps preserves a decorated function's name, docstring, and metadata. Always use it when writing decorators, since without it the wrapper hides the original function's identity from tools that introspect it.
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapperApply the decorator to a small function with a docstring, then inspect its name and docstring after decoration to confirm they were preserved:
@log_call
def greet(name):
"""Return a greeting."""
return f"Hello, {name}!"
print(greet("Maya"))
print(greet.__name__)
print(greet.__doc__)The wrapper's print statement fires first, followed by the greeting itself, then the preserved name and docstring of the original greet function:
Calling greet
Hello, Maya!
greet
Return a greeting.Without @wraps, greet.name would be "wrapper" and greet.doc would be None. wraps copies the original function's attributes to the wrapper.
singledispatch: type-based functions
functools.singledispatch creates a function that behaves differently based on the type of its first argument. It is function overloading by type.
from functools import singledispatch
@singledispatch
def format_value(value):
return str(value)
@format_value.register
def _(value: int):
return f"INT:{value}"
@format_value.register
def _(value: list):
return "[" + ", ".join(format_value(v) for v in value) + "]"With the base function and two registered overloads defined, calling format_value with different argument types dispatches to the matching implementation automatically:
print(format_value(42))
print(format_value([1, 2, 3]))
print(format_value("hello"))The int and list arguments hit their registered overloads, while the plain string falls back to the base implementation, which just calls str() on it:
INT:42
[INT:1, INT:2, INT:3]
helloThe base implementation handles unknown types. Registered implementations handle int and list. The function name _ is conventional for singledispatch registrations since the name is never used directly.
Practical example: memoized API client
Use lru_cache to cache API responses and avoid redundant network calls. The print and sleep inside the function simulate the cost of a real network round trip.
from functools import lru_cache
import time
@lru_cache(maxsize=100)
def fetch_user(user_id: int) -> dict:
print(f"Fetching user {user_id} from API...")
time.sleep(0.1)
return {"id": user_id, "name": f"User_{user_id}"}Call fetch_user with a repeated id mixed in among new ones, then inspect the cache statistics to see how many calls actually hit the network:
print(fetch_user(1))
print(fetch_user(2))
print(fetch_user(1))
print(fetch_user.cache_info())The "Fetching..." message only prints for the first two calls; the third call for user 1 returns straight from the cache with no network simulation at all:
Fetching user 1 from API...
{'id': 1, 'name': 'User_1'}
Fetching user 2 from API...
{'id': 2, 'name': 'User_2'}
{'id': 1, 'name': 'User_1'}
CacheInfo(hits=1, misses=2, maxsize=100, currsize=2)The second call to fetch_user(1) returns instantly from the cache. The cache stores up to 100 unique users before evicting old entries.
Common mistakes
Using lru_cache on functions with unhashable arguments. lru_cache requires all arguments to be hashable. Lists and dicts are not hashable and will cause a TypeError. Convert them to tuples or frozensets before calling.
Forgetting maxsize on lru_cache. lru_cache() without arguments defaults to maxsize=128. If your function takes many unique inputs, set maxsize appropriately or use cache() for unlimited caching.
Using reduce when a built-in is clearer. sum(numbers), max(numbers), any(flags), all(flags), and " ".join(words) are more readable than the equivalent reduce calls. Use reduce only when no dedicated built-in covers your use case.
Forgetting @wraps in decorators. Every decorator that returns a new function should use @wraps(func). Without it, debugging tools, documentation generators, and introspection all see the wrapper instead of the original function.
Rune AI
Key Insights
functools.cache(func)memoizes all calls for unlimited caching (Python 3.9+).functools.lru_cache(maxsize=128)caches with an upper bound, evicting least recently used entries.functools.partial(func, arg)creates a new function with some arguments pre-filled.functools.reduce(func, iterable)combines items into a single result.functools.wraps(func)preserves a decorated function's name, docstring, and metadata.functools.singledispatchcreates functions that behave differently based on the first argument's type.
Frequently Asked Questions
When should I use lru_cache vs cache?
How does functools.partial differ from a lambda?
Can I use functools.cache on methods?
Conclusion
functools is the toolkit for working with functions as values in Python. Use lru_cache and cache to speed up repeated calls, partial to fix arguments of existing functions, reduce to combine iterables into a single value, and wraps to preserve metadata in decorators. These tools let you write cleaner, more composable code without reinventing common patterns.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.