functools Module Overview¶
The functools module provides higher-order functions and operations on callable objects. It's essential for functional programming patterns in Python.
Mental Model
functools is Python's toolkit for working on functions rather than with them. It lets you freeze arguments (partial), cache results (lru_cache, cache), preserve metadata across decorators (wraps), fold sequences (reduce), and dispatch by type (singledispatch). Whenever you need to transform or augment a callable, check functools first.
```python import functools
or¶
from functools import partial, wraps, lru_cache, cache, reduce ```
Module Contents at a Glance¶
| Function | Purpose | Python Version |
|---|---|---|
partial |
Freeze function arguments | 2.5+ |
reduce |
Cumulative fold operation | 2.6+ |
wraps |
Preserve decorator metadata | 2.5+ |
lru_cache |
Bounded memoization (LRU) | 3.2+ |
cache |
Unbounded memoization | 3.9+ |
total_ordering |
Complete comparison ops | 2.7+ |
singledispatch |
Type-based overloading | 3.4+ |
cached_property |
Lazy cached attribute | 3.8+ |
cmp_to_key |
Legacy comparison adapter | 2.7+ |
How functools Fits into Python¶
Relationship to Other Modules¶
``` functools — higher-order functions, caching, dispatch ├── partial — similar to lambda but preserves metadata ├── reduce — moved here from built-ins in Python 3 ├── wraps — essential companion to decorators (Ch 5.6) ├── lru_cache — memoization with eviction policy ├── cache — simpler memoization (Python 3.9+) └── singledispatch — type-based dispatch
operator — function versions of operators (Ch 5.4) itertools — iterator combinators (Ch 7.2) ```
Core Theme: Functions That Transform Functions¶
Every tool in functools takes a callable and produces a new callable:
```python from functools import partial, lru_cache, wraps
partial: fix arguments → new function¶
square = partial(pow, exp=2)
lru_cache: add memoization → cached function¶
@lru_cache(maxsize=128) def fib(n): ...
wraps: copy metadata → wrapper with correct identity¶
def decorator(func): @wraps(func) def wrapper(args, kwargs): return func(args, **kwargs) return wrapper ```
Quick Examples¶
partial — Freeze Arguments¶
```python 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)) # 125 ```
reduce — Fold a Sequence¶
```python from functools import reduce import operator
numbers = [1, 2, 3, 4, 5] product = reduce(operator.mul, numbers) print(product) # 120 ```
lru_cache — Memoize Results¶
```python from functools import lru_cache
@lru_cache(maxsize=128) def fibonacci(n): if n < 2: return n return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Instant print(fibonacci.cache_info())
CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)¶
```
wraps — Preserve Metadata in Decorators¶
```python from functools import wraps
def timer(func): @wraps(func) def wrapper(args, kwargs): import time start = time.perf_counter() result = func(args, **kwargs) print(f"{func.name}: {time.perf_counter() - start:.4f}s") return result return wrapper
@timer def process(data): """Process data.""" return sum(data)
print(process.name) # 'process' (not 'wrapper') ```
total_ordering — Fill in Comparisons¶
```python from functools import total_ordering
@total_ordering class Version: def init(self, major, minor): self.major = major self.minor = minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
All six comparisons now work¶
v1, v2 = Version(1, 0), Version(2, 0) print(v1 < v2) # True (defined) print(v1 <= v2) # True (auto-generated) print(v1 > v2) # False (auto-generated) ```
singledispatch — Type-Based Overloading¶
```python from functools import singledispatch
@singledispatch def process(data): raise NotImplementedError(f"Cannot process {type(data)}")
@process.register(list) def _(data): return [x * 2 for x in data]
@process.register(str) def _(data): return data.upper()
print(process([1, 2, 3])) # [2, 4, 6] print(process("hello")) # HELLO ```
Choosing the Right Tool¶
| Need | Tool | Example |
|---|---|---|
| Fix some arguments | partial |
square = partial(pow, exp=2) |
| Cache function results | lru_cache / cache |
Recursive DP, expensive queries |
| Write a decorator | wraps |
Logging, timing, retry decorators |
| Fold sequence to value | reduce |
Product, flatten, compose |
| Auto-generate comparisons | total_ordering |
Custom sortable classes |
| Dispatch by type | singledispatch |
Format different data types |
| Cache a property | cached_property |
Expensive computed attributes |
Section Map¶
Each function has a dedicated page with detailed coverage:
- functools.cache — unbounded memoization for recursive algorithms
- functools.lru_cache — bounded memoization with LRU eviction
- functools.partial — partial function application
- functools.reduce — cumulative fold operations
- functools.wraps — metadata preservation for decorators
- functools.total_ordering — auto-generate comparison methods
- functools.singledispatch — single-argument type dispatch
Summary¶
| Concept | Key Point |
|---|---|
| Module purpose | Higher-order functions and callable transformations |
| Most used | wraps, lru_cache, partial |
| Caching | cache (unbounded) vs lru_cache (bounded) |
| Decorators | Always use @wraps(func) |
| Folding | reduce works but prefer built-ins when available |
| Dispatch | singledispatch avoids if/elif type chains |
Key Takeaways:
functoolsis the Swiss army knife for functional programming in Pythonwrapsandlru_cacheare used daily in production codepartialcreates cleaner specialized functions than lambdasreduceis powerful but often less readable than built-in alternativestotal_orderingandsingledispatchreduce boilerplate in OOP code
Exercises¶
Exercise 1.
Write a decorator using functools.wraps that logs the name and arguments of any function call to a list called log. Apply it to two different functions, call each once, and print the log.
Solution to Exercise 1
from functools import wraps
log = []
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
log.append(f"{func.__name__}({args}, {kwargs})")
return func(*args, **kwargs)
return wrapper
@logger
def add(a, b):
return a + b
@logger
def greet(name):
return f"Hello, {name}"
add(3, 4)
greet("Alice")
print(log)
# ["add((3, 4), {})", "greet(('Alice',), {})"]
Exercise 2.
Use functools.partial to create a json_dumps_pretty function from json.dumps that always uses indent=2 and sort_keys=True. Demonstrate by serializing a dictionary with nested data.
Solution to Exercise 2
import json
from functools import partial
json_dumps_pretty = partial(json.dumps, indent=2, sort_keys=True)
data = {"name": "Alice", "scores": [95, 87, 91], "active": True}
print(json_dumps_pretty(data))
Exercise 3.
Combine three functools features in one example: use @lru_cache to memoize a recursive function, functools.reduce to aggregate results, and functools.partial to create a specialized version of the aggregation. Specifically, memoize a fibonacci(n) function, then use reduce with operator.add to sum the first 20 Fibonacci numbers, and use partial to create a sum_fibs function that always sums a fixed range.
Solution to Exercise 3
from functools import lru_cache, partial, reduce
import operator
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Sum first 20 Fibonacci numbers using reduce
fibs = [fibonacci(i) for i in range(20)]
total = reduce(operator.add, fibs)
print(f"Sum of first 20 fibs: {total}") # 10945
# Create a reusable partial for summing any list
sum_list = partial(reduce, operator.add)
print(sum_list(fibs)) # 10945