Python functools: partial, lru_cache, reduce, and More
Master Python's functools module with practical examples of partial, lru_cache, reduce, singledispatch, cached_property, and wraps.
What you'll learn
- ✓Using partial to create specialized functions
- ✓Caching expensive computations with lru_cache
- ✓Implementing function dispatch with singledispatch
- ✓Applying reduce, wraps, and total_ordering
Prerequisites
- •Basic Python functions and decorators
- •Understanding of closures
- •Familiarity with iterables
Why functools Matters
The functools module provides higher-order functions and operations on callable objects. It bridges the gap between Python’s object-oriented nature and functional programming patterns. Instead of writing boilerplate code for caching, dispatching, or adapting functions, functools gives you battle-tested tools.
functools.partial: Fix Function Arguments
partial creates a new function by freezing some arguments of an existing function:
from functools import partial
def power(base, exponent):
return base ** exponent
# Create specialized versions
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25
print(cube(3)) # 27
# Useful with higher-order functions
numbers = [1, 2, 3, 4, 5]
squares = list(map(square, numbers))
print(squares) # [1, 4, 9, 16, 25]
A practical example with logging:
from functools import partial
import logging
def log_message(level, component, message):
logging.log(level, f"[{component}] {message}")
# Create component-specific loggers
db_info = partial(log_message, logging.INFO, "database")
db_error = partial(log_message, logging.ERROR, "database")
api_info = partial(log_message, logging.INFO, "api")
db_info("Connection established")
db_error("Query timeout after 30s")
api_info("Request received at /users")
partial is cleaner than lambdas for this purpose and plays nicely with pickle, multiprocessing, and debugging (it preserves the function name and arguments).
from functools import partial
# Partial objects are inspectable
square = partial(power, exponent=2)
print(square.func) # <function power at ...>
print(square.args) # ()
print(square.keywords) # {'exponent': 2}
functools.lru_cache: Memoize Function Results
lru_cache caches function return values based on the arguments. Subsequent calls with the same arguments return the cached result instantly:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Without cache: exponential time
# With cache: linear time
print(fibonacci(100)) # 354224848179261915075
# Inspect cache statistics
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# Clear the cache
fibonacci.cache_clear()
Use maxsize=None for an unbounded cache (useful when you know the input space is finite):
from functools import lru_cache
@lru_cache(maxsize=None)
def count_paths(m, n):
"""Count paths in an m x n grid (top-left to bottom-right)."""
if m == 1 or n == 1:
return 1
return count_paths(m - 1, n) + count_paths(m, n - 1)
print(count_paths(20, 20)) # 35345263800
Important caveat: arguments must be hashable. Lists, dicts, and sets cannot be used as arguments to cached functions.
from functools import lru_cache
@lru_cache
def process(items):
return sum(items)
# Works with tuples (hashable)
process((1, 2, 3))
# Fails with lists (unhashable)
try:
process([1, 2, 3])
except TypeError as e:
print(e) # unhashable type: 'list'
functools.cache (Python 3.9+)
cache is a simpler alias for lru_cache(maxsize=None):
from functools import cache
@cache
def expensive_computation(x, y):
print(f"Computing {x} + {y}")
return x + y
expensive_computation(1, 2) # Computing 1 + 2 -> 3
expensive_computation(1, 2) # Returns 3 without printing
functools.cached_property (Python 3.8+)
cached_property transforms a method into a property that is computed once and then cached as an instance attribute:
from functools import cached_property
import statistics
class Dataset:
def __init__(self, values):
self._values = list(values)
@cached_property
def stats(self):
print("Computing statistics...")
return {
"mean": statistics.mean(self._values),
"median": statistics.median(self._values),
"stdev": statistics.stdev(self._values),
}
@cached_property
def sorted_values(self):
print("Sorting values...")
return sorted(self._values)
data = Dataset([45, 23, 67, 12, 89, 34, 56])
print(data.stats) # Computing statistics... {dict}
print(data.stats) # Returns cached result
print(data.sorted_values) # Sorting values... [12, 23, ...]
Unlike lru_cache, cached_property works per instance and the cached value lives in the instance __dict__. You can invalidate the cache by deleting the attribute:
del data.stats # Remove cached value
print(data.stats) # Recomputes
functools.reduce: Cumulative Operations
reduce applies a two-argument function cumulatively to the items of a sequence:
from functools import reduce
# Sum of all numbers (equivalent to sum())
total = reduce(lambda a, b: a + b, [1, 2, 3, 4, 5])
print(total) # 15
# Product of all numbers
product = reduce(lambda a, b: a * b, [1, 2, 3, 4, 5])
print(product) # 120
# Find the longest string
words = ["Python", "is", "a", "wonderful", "language"]
longest = reduce(lambda a, b: a if len(a) >= len(b) else b, words)
print(longest) # wonderful
reduce accepts an optional initial value:
from functools import reduce
# Flatten a list of lists
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(lambda acc, lst: acc + lst, nested, [])
print(flat) # [1, 2, 3, 4, 5, 6]
# Build a dictionary from pairs
pairs = [("a", 1), ("b", 2), ("c", 3)]
result = reduce(lambda d, pair: {**d, pair[0]: pair[1]}, pairs, {})
print(result) # {'a': 1, 'b': 2, 'c': 3}
A practical example that composes functions:
from functools import reduce
def compose(*functions):
"""Compose multiple functions: compose(f, g, h)(x) = f(g(h(x)))"""
return reduce(lambda f, g: lambda x: f(g(x)), functions)
double = lambda x: x * 2
increment = lambda x: x + 1
square = lambda x: x ** 2
transform = compose(double, increment, square)
print(transform(3)) # double(increment(square(3))) = double(10) = 20
functools.wraps: Preserve Function Metadata
When you write decorators, the wrapper function replaces the original. wraps copies the original function’s metadata to the wrapper:
from functools import wraps
import time
# Without wraps: metadata is lost
def timer_bad(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
# With wraps: metadata is preserved
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timer
def fetch_data(url):
"""Fetch data from a URL."""
time.sleep(0.1)
return f"data from {url}"
print(fetch_data.__name__) # fetch_data (not 'wrapper')
print(fetch_data.__doc__) # Fetch data from a URL.
Without @wraps, fetch_data.__name__ would be "wrapper", which breaks introspection tools, documentation generators, and debugging.
functools.singledispatch: Type-Based Dispatch
singledispatch turns a function into a generic function that dispatches on the type of its first argument:
from functools import singledispatch
@singledispatch
def serialize(obj):
raise TypeError(f"Cannot serialize {type(obj)}")
@serialize.register(str)
def _(obj):
return f'"{obj}"'
@serialize.register(int)
def _(obj):
return str(obj)
@serialize.register(float)
def _(obj):
return f"{obj:.2f}"
@serialize.register(list)
def _(obj):
items = ", ".join(serialize(item) for item in obj)
return f"[{items}]"
@serialize.register(dict)
def _(obj):
pairs = ", ".join(f"{serialize(k)}: {serialize(v)}" for k, v in obj.items())
return "{" + pairs + "}"
print(serialize("hello")) # "hello"
print(serialize(42)) # 42
print(serialize(3.14159)) # 3.14
print(serialize([1, "two", 3.0])) # [1, "two", 3.00]
print(serialize({"a": 1})) # {"a": 1}
You can also use type annotations (Python 3.7+):
from functools import singledispatch
from datetime import datetime, date
@singledispatch
def format_value(value):
return str(value)
@format_value.register
def _(value: datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
@format_value.register
def _(value: date):
return value.strftime("%Y-%m-%d")
@format_value.register
def _(value: float):
return f"{value:,.2f}"
print(format_value(datetime(2026, 7, 6, 14, 30))) # 2026-07-06 14:30:00
print(format_value(1234567.89)) # 1,234,567.89
For methods, use singledispatchmethod (Python 3.8+):
from functools import singledispatchmethod
class Formatter:
@singledispatchmethod
def format(self, value):
return str(value)
@format.register
def _(self, value: int):
return f"Integer: {value}"
@format.register
def _(self, value: list):
return f"List with {len(value)} items"
fmt = Formatter()
print(fmt.format(42)) # Integer: 42
print(fmt.format([1, 2, 3])) # List with 3 items
functools.total_ordering: Complete Comparison Methods
Define __eq__ and one other comparison method, and total_ordering fills in the rest:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
def __repr__(self):
return f"Version({self.major}.{self.minor}.{self.patch})"
v1 = Version(1, 2, 3)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 3)
print(v1 < v2) # True
print(v1 <= v3) # True (generated by total_ordering)
print(v2 > v1) # True (generated by total_ordering)
print(v1 >= v3) # True (generated by total_ordering)
print(v1 == v3) # True
versions = [Version(2, 0, 0), Version(1, 5, 3), Version(1, 2, 0)]
print(sorted(versions)) # [Version(1.2.0), Version(1.5.3), Version(2.0.0)]
functools.cmp_to_key: Legacy Sort Functions
Convert an old-style comparison function to a key function for sorted:
from functools import cmp_to_key
def compare_versions(a, b):
"""Old-style comparison: returns negative, zero, or positive."""
for a_part, b_part in zip(a.split("."), b.split(".")):
diff = int(a_part) - int(b_part)
if diff != 0:
return diff
return 0
versions = ["1.10.2", "1.2.3", "2.0.0", "1.10.1"]
sorted_versions = sorted(versions, key=cmp_to_key(compare_versions))
print(sorted_versions) # ['1.2.3', '1.10.1', '1.10.2', '2.0.0']
Combining functools Tools
These tools work well together:
from functools import lru_cache, partial, reduce, wraps
def retry(max_attempts=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}")
return wrapper
return decorator
@retry(max_attempts=3)
@lru_cache(maxsize=64)
def fetch_config(key):
"""Fetch configuration value with retry and caching."""
import random
if random.random() < 0.3:
raise ConnectionError("Network timeout")
return f"value_for_{key}"
# The decorator order matters: retry wraps the cached function
result = fetch_config("database_url")
print(result)
print(fetch_config.__name__) # fetch_config (preserved by @wraps)
Wrapping Up
The functools module is one of Python’s most practical standard library modules. partial lets you create specialized functions without lambdas. lru_cache and cached_property add memoization with minimal code. singledispatch provides clean type-based dispatch. reduce enables cumulative operations. wraps preserves metadata across decorators. And total_ordering saves you from writing repetitive comparison methods. These tools reduce boilerplate and make your code more expressive, readable, and efficient.
Related articles
- Python Python functools Deep Dive: Beyond lru_cache
Explore the functools module beyond caching. Learn partial, reduce, singledispatch, cached_property, and total_ordering with practical examples.
- Python Python itertools Recipes: Practical Patterns for Efficient Iteration
Master Python's itertools module with practical recipes for grouping, windowing, chaining, and processing iterables efficiently.
- Django Django Caching Strategies
Compare per-view, template fragment, low-level, and per-site caching in Django and learn when each pays off.
- Python Python Concurrency: asyncio vs threading vs multiprocessing
Compare Python's concurrency models side by side. Learn when to use asyncio, threading, or multiprocessing with practical benchmarks and real-world examples.